diff --git a/govtool/frontend/.eslintrc.cjs b/govtool/frontend/.eslintrc.cjs index 130d9f325..8a7824a55 100644 --- a/govtool/frontend/.eslintrc.cjs +++ b/govtool/frontend/.eslintrc.cjs @@ -44,6 +44,7 @@ module.exports = { "no-nested-ternary": "off", "no-unused-vars": "off", + "no-use-before-define": "off", "comma-dangle": "off", "operator-linebreak": "off", "implicit-arrow-linebreak": "off", diff --git a/govtool/frontend/src/App.tsx b/govtool/frontend/src/App.tsx index 77ef3af19..7db190366 100644 --- a/govtool/frontend/src/App.tsx +++ b/govtool/frontend/src/App.tsx @@ -30,14 +30,13 @@ import { removeItemFromLocalStorage, } from "@utils"; import { SetupInterceptors } from "./services"; -import { useGetVoterInfo, useWalletConnectionListener } from "./hooks"; +import { useWalletConnectionListener } from "./hooks"; import { RegisterAsSoleVoter } from "./pages/RegisterAsSoleVoter"; import { CreateGovernanceAction } from "./pages/CreateGovernanceAction"; export default () => { - const { enable, setVoter, setIsDrepLoading } = useCardano(); + const { enable } = useCardano(); const navigate = useNavigate(); - const { data } = useGetVoterInfo(); const { modal, openModal, modals } = useModal(); useWalletConnectionListener(); @@ -46,14 +45,6 @@ export default () => { SetupInterceptors(navigate); }, []); - useEffect(() => { - setIsDrepLoading(true); - setVoter(data); - const timer = setTimeout(() => setIsDrepLoading(false), 1000); - - return () => clearTimeout(timer); - }, [data?.isRegisteredAsDRep, data?.isRegisteredAsSoleVoter]); - const checkTheWalletIsActive = useCallback(() => { const hrefCondition = window.location.pathname === PATHS.home || @@ -135,8 +126,8 @@ export default () => { handleClose={ !modals[modal.type].preventDismiss ? callAll(modals[modal.type]?.onClose, () => - openModal({ type: "none", state: null }), - ) + openModal({ type: "none", state: null }), + ) : undefined } > diff --git a/govtool/frontend/src/components/atoms/VotingPowerChips.tsx b/govtool/frontend/src/components/atoms/VotingPowerChips.tsx index 56bd5625a..4e5381e56 100644 --- a/govtool/frontend/src/components/atoms/VotingPowerChips.tsx +++ b/govtool/frontend/src/components/atoms/VotingPowerChips.tsx @@ -6,31 +6,33 @@ import { useCardano } from "@context"; import { useGetAdaHolderVotingPowerQuery, useGetDRepVotingPowerQuery, + useGetVoterInfo, useScreenDimension, useTranslation, } from "@hooks"; import { correctAdaFormat } from "@utils"; export const VotingPowerChips = () => { - const { voter, stakeKey, isDrepLoading } = useCardano(); - const { dRepVotingPower, isDRepVotingPowerLoading } = - useGetDRepVotingPowerQuery(); - const { votingPower, powerIsLoading } = - useGetAdaHolderVotingPowerQuery(stakeKey); + const { stakeKey, isEnableLoading } = useCardano(); + const { dRepVotingPower } = useGetDRepVotingPowerQuery(); + const { votingPower } = useGetAdaHolderVotingPowerQuery(stakeKey); const { isMobile, screenWidth } = useScreenDimension(); const { t } = useTranslation(); + const { voter } = useGetVoterInfo(); return ( {voter?.isRegisteredAsDRep && ( { {t("votingPower")} )} - {(voter?.isRegisteredAsDRep && isDRepVotingPowerLoading) || - (!voter?.isRegisteredAsDRep && powerIsLoading) || - isDrepLoading ? ( - + {(voter?.isRegisteredAsDRep && dRepVotingPower === undefined) || + (!voter?.isRegisteredAsDRep && votingPower === undefined) || + isEnableLoading || + !voter ? ( + ) : ( (false); const { isMobile, screenWidth } = useScreenDimension(); const { openModal } = useModal(); - const { voter } = useCardano(); const { t } = useTranslation(); + const { voter } = useGetVoterInfo(); const { areFormErrors, @@ -74,28 +79,27 @@ export const VoteActionForm = ({ [state], ); - const renderChangeVoteButton = useMemo( - () => ( - - {t("govActions.changeVote")} - - ), + const renderChangeVoteButton = useMemo(() => ( + + {t('govActions.changeVote')} + + ), [confirmVote, areFormErrors, vote, isVoteLoading], ); @@ -247,16 +251,16 @@ export const VoteActionForm = ({ {t("govActions.selectDifferentOption")} {(state?.vote && state?.vote !== vote) || - (voteFromEP && voteFromEP !== vote) ? ( - - {isMobile ? renderChangeVoteButton : renderCancelButton} - - {isMobile ? renderCancelButton : renderChangeVoteButton} - + (voteFromEP && voteFromEP !== vote) ? ( + + {isMobile ? renderChangeVoteButton : renderCancelButton} + + {isMobile ? renderCancelButton : renderChangeVoteButton} + ) : ( { const { buildDRepRetirementCert, buildSignSubmitConwayCertTx, - delegateTo, - delegateTransaction, dRepID, + isEnableLoading, dRepIDBech32, - govActionTransaction, - isDrepLoading, isPendingTransaction, - registerTransaction, - soleVoterTransaction, + pendingTransaction, stakeKey, - voter, } = useCardano(); const navigate = useNavigate(); - const { currentDelegation, isCurrentDelegationLoading } = + const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); const { screenWidth } = useScreenDimension(); const { openModal } = useModal(); const [isRetirementLoading, setIsRetirementLoading] = useState(false); - const { votingPower, powerIsLoading } = + const { votingPower } = useGetAdaHolderVotingPowerQuery(stakeKey); const { t } = useTranslation(); + const { voter } = useGetVoterInfo(); const retireAsDrep = useCallback(async () => { try { setIsRetirementLoading(true); const isPendingTx = isPendingTransaction(); + if (isPendingTx) return; - const certBuilder = await buildDRepRetirementCert(); + if (!voter?.deposit) throw new Error('Can not get deposit'); + + const certBuilder = await buildDRepRetirementCert( + voter.deposit.toString(), + ); const result = await buildSignSubmitConwayCertTx({ certBuilder, - type: "registration", - registrationType: "retirement", + type: 'retireAsDrep', + voterDeposit: voter.deposit.toString(), }); if (result) { openModal({ - type: "statusModal", + type: 'statusModal', state: { - status: "success", - title: t("modals.retirement.title"), - message: t("modals.retirement.message"), - link: "https://adanordic.com/latest_transactions", - buttonText: t("modals.common.goToDashboard"), - dataTestId: "retirement-transaction-submitted-modal", + status: 'success', + title: t('modals.retirement.title'), + message: t('modals.retirement.message'), + link: `https://adanordic.com/latest_transactions`, + buttonText: t('modals.common.goToDashboard'), + dataTestId: 'retirement-transaction-submitted-modal', }, }); } @@ -70,13 +72,13 @@ export const DashboardCards = () => { const errorMessage = error.info ? error.info : error; openModal({ - type: "statusModal", + type: 'statusModal', state: { - status: "warning", + status: 'warning', message: errorMessage, - buttonText: t("modals.common.goToDashboard"), - title: t("modals.common.oops"), - dataTestId: "retirement-transaction-error-modal", + buttonText: t('modals.common.goToDashboard'), + title: t('modals.common.oops'), + dataTestId: 'retirement-transaction-error-modal', }, }); } finally { @@ -87,6 +89,7 @@ export const DashboardCards = () => { buildSignSubmitConwayCertTx, isPendingTransaction, openModal, + voter?.deposit, ]); const delegationDescription = useMemo(() => { @@ -144,12 +147,15 @@ export const DashboardCards = () => { if (currentDelegation) { return "dRep"; } - return "not_delegated"; + return 'not_delegated'; }, [currentDelegation, dRepID, votingPower]); const progressDescription = useMemo(() => { const correctAdaRepresentation = correctAdaFormat(votingPower); - if (delegateTo === dRepID) { + if (!pendingTransaction.delegate) return; + const { resourceId } = pendingTransaction.delegate; + + if (resourceId === dRepID) { return ( { /> ); } - if (delegateTo === "no confidence") { + if (resourceId === 'no confidence') { return ( { /> ); } - if (delegateTo === "abstain") { + if (resourceId === 'abstain') { return ( { /> ); } - if (delegateTo) { + if (resourceId) { return ( { /> ); } - }, [delegateTo, dRepID, votingPower]); + }, [pendingTransaction, dRepID, votingPower]); const navigateTo = useCallback( (path: string) => { @@ -193,24 +199,25 @@ export const DashboardCards = () => { ); const onClickGovernanceActionCardActionButton = useCallback(() => { - if (govActionTransaction.transactionHash) { + if (pendingTransaction.createGovAction) { navigate(PATHS.dashboardGovernanceActions); return; } navigate(PATHS.createGovernanceAction); - }, [govActionTransaction.transactionHash, navigate]); + }, [pendingTransaction.createGovAction, navigate]); const displayedDelegationId = useMemo(() => { const restrictedNames = [ dRepID, - "drep_always_abstain", - "drep_always_no_confidence", - "abstain", - "no confidence", + 'drep_always_abstain', + 'drep_always_no_confidence', + 'abstain', + 'no confidence', ]; - if (delegateTransaction?.transactionHash) { + if (pendingTransaction.delegate) { + const delegateTo = pendingTransaction.delegate.resourceId; if (!restrictedNames.includes(delegateTo)) { - return delegateTo.includes("drep") + return delegateTo.includes('drep') ? delegateTo : formHexToBech32(delegateTo); } @@ -220,112 +227,82 @@ export const DashboardCards = () => { return formHexToBech32(currentDelegation); } return undefined; - }, [ - currentDelegation, - dRepID, - delegateTo, - delegateTransaction, - formHexToBech32, - ]); + }, [currentDelegation, dRepID, pendingTransaction, formHexToBech32]); const registrationCardDescription = useMemo(() => { - if (registerTransaction.transactionHash) { - switch (registerTransaction.type) { - case "retirement": - return t("dashboard.registration.retirementInProgress"); - case "registration": - return t("dashboard.registration.registrationInProgress"); - default: - return t("dashboard.registration.metadataUpdateInProgress"); - } - } else if (voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep) { - return t("dashboard.registration.holdersCanDelegate"); - } else { - return t("dashboard.registration.ifYouWant"); - } + if (pendingTransaction.registerAsDrep) return t('dashboard.registration.registrationInProgress'); + + if (pendingTransaction.retireAsDrep) return t('dashboard.registration.retirementInProgress'); + + if (pendingTransaction.updateMetaData) return t('dashboard.registration.metadataUpdateInProgress'); + + if (voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep) return t('dashboard.registration.holdersCanDelegate'); + + return t('dashboard.registration.ifYouWant'); }, [ - registerTransaction.transactionHash, - registerTransaction.type, + pendingTransaction, voter?.isRegisteredAsDRep, voter?.wasRegisteredAsDRep, ]); const soleVoterCardDescription = useMemo(() => { - if (soleVoterTransaction.transactionHash) { - switch (soleVoterTransaction.type) { - case "retirement": - return "dashboard.soleVoter.retirementInProgress"; - default: - return "dashboard.soleVoter.registrationInProgress"; - } - } else if (voter?.isRegisteredAsSoleVoter) { - return "dashboard.soleVoter.isRegisteredDescription"; - } else if (voter?.wasRegisteredAsSoleVoter) { - return "dashboard.soleVoter.wasRegisteredDescription"; - } else { - return "dashboard.soleVoter.registerDescription"; - } + if (pendingTransaction.registerAsSoleVoter) return 'dashboard.soleVoter.registrationInProgress'; + + if (pendingTransaction.retireAsSoleVoter) return 'dashboard.soleVoter.retirementInProgress'; + + if (voter?.isRegisteredAsSoleVoter) return 'dashboard.soleVoter.isRegisteredDescription'; + + if (voter?.wasRegisteredAsSoleVoter) return 'dashboard.soleVoter.wasRegisteredDescription'; + + return 'dashboard.soleVoter.registerDescription'; }, [ - soleVoterTransaction.transactionHash, - soleVoterTransaction.type, + pendingTransaction, voter?.isRegisteredAsSoleVoter, voter?.wasRegisteredAsSoleVoter, ]); const registrationCardTitle = useMemo(() => { - if (registerTransaction?.transactionHash) { - switch (registerTransaction.type) { - case "retirement": - return t("dashboard.registration.dRepRetirement"); - case "registration": - return t("dashboard.registration.dRepRegistration"); - default: - return t("dashboard.registration.dRepUpdate"); - } - } else if (voter?.isRegisteredAsDRep) { - return t("dashboard.registration.youAreRegistered"); - } else if (voter?.wasRegisteredAsDRep) { - return t("dashboard.registration.registerAgain"); - } else { - return t("dashboard.registration.registerAsDRep"); - } + if (pendingTransaction.retireAsDrep) return t('dashboard.registration.dRepRetirement'); + + if (pendingTransaction.registerAsDrep) return t('dashboard.registration.dRepRegistration'); + + if (pendingTransaction.updateMetaData) return t('dashboard.registration.dRepUpdate'); + + if (voter?.isRegisteredAsDRep) return t('dashboard.registration.youAreRegistered'); + + if (voter?.wasRegisteredAsDRep) return t('dashboard.registration.registerAgain'); + + return t('dashboard.registration.registerAsDRep'); }, [ - registerTransaction?.transactionHash, - registerTransaction.type, + pendingTransaction, voter?.isRegisteredAsDRep, voter?.wasRegisteredAsDRep, ]); const soleVoterCardTitle = useMemo(() => { - if (soleVoterTransaction?.transactionHash) { - switch (soleVoterTransaction.type) { - case "retirement": - return t("dashboard.soleVoter.retirement"); - default: - return t("dashboard.soleVoter.registration"); - } - } else if (voter?.isRegisteredAsSoleVoter) { - return t("dashboard.soleVoter.youAreSoleVoterTitle"); - } else if (voter?.wasRegisteredAsSoleVoter) { - return t("dashboard.soleVoter.wasSoleVoterTitle"); - } else { - return t("dashboard.soleVoter.registerTitle"); - } + if (pendingTransaction.retireAsSoleVoter) return t('dashboard.soleVoter.retirement'); + + if (pendingTransaction.registerAsSoleVoter) return t('dashboard.soleVoter.registration'); + + if (voter?.isRegisteredAsSoleVoter) return t('dashboard.soleVoter.youAreSoleVoterTitle'); + + if (voter?.wasRegisteredAsSoleVoter) return t('dashboard.soleVoter.wasSoleVoterTitle'); + + return t('dashboard.soleVoter.registerTitle'); }, [ - soleVoterTransaction?.transactionHash, - soleVoterTransaction.type, + pendingTransaction, voter?.isRegisteredAsSoleVoter, voter?.isRegisteredAsSoleVoter, ]); - return isDrepLoading ? ( + return isEnableLoading || !currentDelegation || !voter || !votingPower ? ( @@ -334,13 +311,13 @@ export const DashboardCards = () => { = 1728 - ? "repeat(3, minmax(300px, 570px))" - : "repeat(2, minmax(300px, 530px))", + ? "repeat(3, minmax(300px, 570px))" + : "repeat(2, minmax(300px, 530px))", justifyContent: screenWidth < 1024 ? "center" : "flex-start", px: screenWidth < 640 ? 2 : 5, py: 3, @@ -350,56 +327,55 @@ export const DashboardCards = () => { {/* DELEGATION CARD */} navigateTo(PATHS.delegateTodRep)} firstButtonLabel={ - delegateTransaction?.transactionHash - ? "" + pendingTransaction.delegate + ? '' : currentDelegation - ? t("dashboard.delegation.changeDelegation") - : t("delegate") + ? t("dashboard.delegation.changeDelegation") + : t("delegate") } - firstButtonVariant={currentDelegation ? "outlined" : "contained"} + firstButtonVariant={currentDelegation ? 'outlined' : 'contained'} imageURL={IMAGES.govActionDelegateImage} cardId={displayedDelegationId} - inProgress={!!delegateTransaction?.transactionHash} - cardTitle={t("dashboard.delegation.dRepDelegatedTo")} + inProgress={!!pendingTransaction.delegate} + cardTitle={t('dashboard.delegation.dRepDelegatedTo')} secondButtonAction={ - delegateTransaction?.transactionHash + pendingTransaction.delegate ? () => openInNewTab("https://adanordic.com/latest_transactions") : () => - openInNewTab( - "https://docs.sanchogov.tools/faqs/ways-to-use-your-voting-power", - ) + openInNewTab( + "https://docs.sanchogov.tools/faqs/ways-to-use-your-voting-power", + ) } secondButtonLabel={ - delegateTransaction?.transactionHash - ? t("seeTransaction") + pendingTransaction.delegate + ? t('seeTransaction') : currentDelegation - ? "" - : t("learnMore") + ? "" + : t("learnMore") } title={ - delegateTransaction?.transactionHash ? ( - t("dashboard.delegation.votingPowerDelegation") + pendingTransaction.delegate ? ( + t('dashboard.delegation.votingPowerDelegation') ) : currentDelegation ? ( ) : ( - t("dashboard.delegation.useYourVotingPower") + t('dashboard.delegation.useYourVotingPower') ) } /> @@ -407,23 +383,23 @@ export const DashboardCards = () => { {/* REGISTARTION AS DREP CARD */} { } firstButtonIsLoading={isRetirementLoading} firstButtonLabel={ - registerTransaction?.transactionHash - ? "" + pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep + ? '' : t( - `dashboard.registration.${ - voter?.isRegisteredAsDRep ? "retire" : "register" - }`, - ) + `dashboard.registration.${voter?.isRegisteredAsDRep ? "retire" : "register" + }`, + ) + } + inProgress={ + !!( + pendingTransaction.registerAsDrep || + pendingTransaction.retireAsDrep || + pendingTransaction.updateMetaData + ) } - inProgress={!!registerTransaction?.transactionHash} imageURL={IMAGES.govActionRegisterImage} secondButtonAction={ - registerTransaction?.transactionHash - ? () => openInNewTab("https://adanordic.com/latest_transactions") + pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep + ? () => openInNewTab('https://adanordic.com/latest_transactions') : voter?.isRegisteredAsDRep - ? () => { + ? () => { navigateTo(PATHS.updateMetadata); } - : () => + : () => openInNewTab( "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep", ) } secondButtonLabel={ - registerTransaction?.transactionHash - ? t("seeTransaction") + pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep + ? t('seeTransaction') : voter?.isRegisteredAsDRep - ? t("dashboard.registration.changeMetadata") - : t("learnMore") + ? t("dashboard.registration.changeMetadata") + : t("learnMore") } cardId={ voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep ? dRepIDBech32 - : "" + : '' } cardTitle={ voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep - ? t("myDRepId") - : "" + ? t('myDRepId') + : '' } title={registrationCardTitle} /> @@ -478,11 +459,14 @@ export const DashboardCards = () => { {/* SOLE VOTER CARD */} { /> } firstButtonLabel={ - soleVoterTransaction?.transactionHash - ? "" + pendingTransaction.registerAsSoleVoter + ? '' : t( - voter?.isRegisteredAsSoleVoter - ? "dashboard.soleVoter.retire" - : voter?.wasRegisteredAsSoleVoter + voter?.isRegisteredAsSoleVoter + ? "dashboard.soleVoter.retire" + : voter?.wasRegisteredAsSoleVoter ? "dashboard.soleVoter.reRegister" : "dashboard.soleVoter.register", - ) + ) } firstButtonAction={() => navigateTo( @@ -510,7 +494,7 @@ export const DashboardCards = () => { ) } firstButtonVariant={ - voter?.isRegisteredAsSoleVoter ? "outlined" : "contained" + voter?.isRegisteredAsSoleVoter ? 'outlined' : 'contained' } secondButtonLabel={t("learnMore")} secondButtonAction={() => @@ -525,37 +509,33 @@ export const DashboardCards = () => { {/* GOV ACTIONS LIST CARD */} navigate(PATHS.dashboardGovernanceActions)} firstButtonLabel={t( - `dashboard.govActions.${ - voter?.isRegisteredAsDRep ? "reviewAndVote" : "view" + `dashboard.govActions.${voter?.isRegisteredAsDRep ? 'reviewAndVote' : 'view' }`, )} imageURL={IMAGES.govActionListImage} - title={t("dashboard.govActions.title")} + title={t('dashboard.govActions.title')} /> {/* GOV ACTIONS LIST CARD END */} {/* GOV ACTIONS LIST CARD */} - openInNewTab( - "https://docs.sanchogov.tools/faqs/what-is-a-governance-action", - ) - } + secondButtonAction={() => openInNewTab( + "https://docs.sanchogov.tools/faqs/what-is-a-governance-action", + )} secondButtonVariant="outlined" imageURL={IMAGES.proposeGovActionImage} - title={t("dashboard.proposeGovernanceAction.title")} + title={t('dashboard.proposeGovernanceAction.title')} /> {/* GOV ACTIONS LIST CARD END */} diff --git a/govtool/frontend/src/components/organisms/DashboardDrawerMobile.tsx b/govtool/frontend/src/components/organisms/DashboardDrawerMobile.tsx index c107986dc..ee75b1df1 100644 --- a/govtool/frontend/src/components/organisms/DashboardDrawerMobile.tsx +++ b/govtool/frontend/src/components/organisms/DashboardDrawerMobile.tsx @@ -2,9 +2,8 @@ import { Box, Grid, IconButton, SwipeableDrawer } from "@mui/material"; import { Background, Link } from "@atoms"; import { CONNECTED_NAV_ITEMS, ICONS } from "@consts"; -import { useCardano } from "@context"; import { DRepInfoCard, WalletInfoCard } from "@molecules"; -import { useScreenDimension } from "@hooks"; +import { useGetVoterInfo, useScreenDimension } from "@hooks"; import { openInNewTab } from "@utils"; import { DashboardDrawerMobileProps } from "./types"; @@ -18,7 +17,7 @@ export const DashboardDrawerMobile = ({ setIsDrawerOpen, }: DashboardDrawerMobileProps) => { const { screenWidth } = useScreenDimension(); - const { voter } = useCardano(); + const { voter } = useGetVoterInfo(); const openDrawer = () => { setIsDrawerOpen(true); diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx index df382e1f6..586005b07 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx @@ -14,9 +14,9 @@ import { } from "@mui/material"; import { ICONS, PATHS } from "@consts"; -import { useCardano } from "@context"; import { useGetProposalQuery, + useGetVoterInfo, useScreenDimension, useTranslation, } from "@hooks"; @@ -28,7 +28,7 @@ import { } from "@utils"; export const DashboardGovernanceActionDetails = () => { - const { voter } = useCardano(); + const { voter } = useGetVoterInfo(); const { state, hash } = useLocation(); const navigate = useNavigate(); const { isMobile, screenWidth } = useScreenDimension(); diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx index 59389f6d3..d865e5e8d 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx @@ -2,13 +2,19 @@ import { useState, useCallback, useEffect } from "react"; import { Box, CircularProgress, Tab, Tabs, styled } from "@mui/material"; import { useLocation } from "react-router-dom"; -import { useCardano } from "@context"; -import { useScreenDimension, useTranslation } from "@hooks"; -import { DataActionsBar } from "@molecules"; +import { GOVERNANCE_ACTIONS_FILTERS } from '@consts'; +import { useCardano } from '@context'; +import { + useGetProposalsQuery, + useGetVoterInfo, + useScreenDimension, + useTranslation, +} from '@hooks'; +import { DataActionsBar } from '@molecules'; import { GovernanceActionsToVote, DashboardGovernanceActionsVotedOn, -} from "@organisms"; +} from '@organisms'; interface TabPanelProps { children?: React.ReactNode; @@ -16,6 +22,10 @@ interface TabPanelProps { value: number; } +const defaultCategories = GOVERNANCE_ACTIONS_FILTERS.map( + (category) => category.key, +); + const CustomTabPanel = (props: TabPanelProps) => { const { children, value, index } = props; @@ -26,8 +36,8 @@ const CustomTabPanel = (props: TabPanelProps) => { id={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} style={{ - display: "flex", - flexDirection: "column", + display: 'flex', + flexDirection: 'column', flex: value !== index ? 0 : 1, }} > @@ -43,32 +53,41 @@ type StyledTabProps = { const StyledTab = styled((props: StyledTabProps) => ( ))(() => ({ - textTransform: "none", + textTransform: 'none', fontWeight: 400, fontSize: 16, - color: "#242232", - "&.Mui-selected": { - color: "#FF640A", + color: '#242232', + '&.Mui-selected': { + color: '#FF640A', fontWeight: 500, }, })); export const DashboardGovernanceActions = () => { - const [searchText, setSearchText] = useState(""); + const [searchText, setSearchText] = useState(''); const [filtersOpen, setFiltersOpen] = useState(false); const [chosenFilters, setChosenFilters] = useState([]); const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const [chosenSorting, setChosenSorting] = useState(''); + const { voter } = useGetVoterInfo(); + const { isMobile } = useScreenDimension(); + const { t } = useTranslation(); + const { isEnableLoading } = useCardano(); + + const queryFilters = + chosenFilters.length > 0 ? chosenFilters : defaultCategories; + + const { proposals, isProposalsLoading } = useGetProposalsQuery({ + filters: queryFilters, + sorting: chosenSorting, + searchPhrase: searchText, + }); const { state } = useLocation(); const [content, setContent] = useState( state && state.isVotedListOnLoad ? 1 : 0, ); - const { voter, isDrepLoading } = useCardano(); - const { isMobile } = useScreenDimension(); - const { t } = useTranslation(); - const handleChange = (_event: React.SyntheticEvent, newValue: number) => { setContent(newValue); }; @@ -93,83 +112,86 @@ export const DashboardGovernanceActions = () => { display="flex" flexDirection="column" > - {isDrepLoading ? ( - - - - ) : ( - <> - - {(voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter) && ( - - + + {!proposals || !voter || isEnableLoading || isProposalsLoading ? ( + + + + ) : ( + <> + {(voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter) && ( + + + + + )} + + + - + + - - )} - - - - - - - - - )} + + + )} + ); }; diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx index 64a108d74..e2b024610 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx @@ -1,16 +1,16 @@ -import { useMemo } from "react"; -import { Box, Typography, CircularProgress } from "@mui/material"; +import { useMemo } from 'react'; +import { Box, Typography, CircularProgress } from '@mui/material'; -import { GovernanceVotedOnCard } from "@molecules"; +import { GovernanceVotedOnCard } from '@molecules'; import { useGetDRepVotesQuery, useScreenDimension, useTranslation, -} from "@hooks"; -import { Slider } from "."; -import { getProposalTypeLabel } from "@/utils/getProposalTypeLabel"; -import { getFullGovActionId } from "@/utils"; -import { useCardano } from "@/context"; +} from '@hooks'; +import { Slider } from '.'; +import { getProposalTypeLabel } from '@/utils/getProposalTypeLabel'; +import { getFullGovActionId } from '@/utils'; +import { useCardano } from '@/context'; interface DashboardGovernanceActionsVotedOnProps { filters: string[]; @@ -25,7 +25,7 @@ export const DashboardGovernanceActionsVotedOn = ({ }: DashboardGovernanceActionsVotedOnProps) => { const { data, areDRepVotesLoading } = useGetDRepVotesQuery(filters, sorting); const { isMobile } = useScreenDimension(); - const { voteTransaction } = useCardano(); + const { pendingTransaction } = useCardano(); const { t } = useTranslation(); const filteredData = useMemo(() => { @@ -42,7 +42,7 @@ export const DashboardGovernanceActionsVotedOn = ({ .filter((entry) => entry.actions.length > 0); } return data; - }, [data, searchPhrase, voteTransaction.transactionHash]); + }, [data, searchPhrase, pendingTransaction.vote]); return areDRepVotesLoading ? ( @@ -52,11 +52,11 @@ export const DashboardGovernanceActionsVotedOn = ({ <> {!data.length ? ( - {t("govActions.youHaventVotedYet")} + {t('govActions.youHaventVotedYet')} ) : !filteredData?.length ? ( - {t("govActions.noResultsForTheSearch")} + {t('govActions.noResultsForTheSearch')} ) : ( <> @@ -77,7 +77,7 @@ export const DashboardGovernanceActionsVotedOn = ({ diff --git a/govtool/frontend/src/components/organisms/DelegateTodRepStepOne.tsx b/govtool/frontend/src/components/organisms/DelegateTodRepStepOne.tsx index c4fb3aef4..6524497d4 100644 --- a/govtool/frontend/src/components/organisms/DelegateTodRepStepOne.tsx +++ b/govtool/frontend/src/components/organisms/DelegateTodRepStepOne.tsx @@ -8,6 +8,7 @@ import { useCardano, useModal } from "@context"; import { useGetAdaHolderCurrentDelegationQuery, useGetAdaHolderVotingPowerQuery, + useGetVoterInfo, useScreenDimension, useTranslation, } from "@hooks"; @@ -21,12 +22,12 @@ interface DelegateProps { export const DelegateTodRepStepOne = ({ setStep }: DelegateProps) => { const navigate = useNavigate(); const { - voter, dRepID, buildSignSubmitConwayCertTx, buildVoteDelegationCert, stakeKey, } = useCardano(); + const { voter } = useGetVoterInfo(); const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); const { openModal, closeModal } = useModal(); const [areOptions, setAreOptions] = useState(false); @@ -93,7 +94,8 @@ export const DelegateTodRepStepOne = ({ setStep }: DelegateProps) => { const certBuilder = await buildVoteDelegationCert(chosenOption); const result = await buildSignSubmitConwayCertTx({ certBuilder, - type: "delegation", + type: "delegate", + resourceId: chosenOption, }); if (result) openSuccessDelegationModal(); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/govtool/frontend/src/components/organisms/Drawer.tsx b/govtool/frontend/src/components/organisms/Drawer.tsx index cc790ae80..b69c1dd51 100644 --- a/govtool/frontend/src/components/organisms/Drawer.tsx +++ b/govtool/frontend/src/components/organisms/Drawer.tsx @@ -1,39 +1,38 @@ -import { Box, Grid } from "@mui/material"; -import { NavLink } from "react-router-dom"; +import { Box, Grid } from '@mui/material'; +import { NavLink } from 'react-router-dom'; import { DrawerLink, Typography } from "@atoms"; import { CONNECTED_NAV_ITEMS, ICONS, IMAGES, PATHS } from "@consts"; -import { useCardano } from "@context"; -import { useTranslation } from "@hooks"; +import { useGetVoterInfo, useTranslation } from "@hooks"; import { WalletInfoCard, DRepInfoCard } from "@molecules"; import { openInNewTab } from "@utils"; export const Drawer = () => { - const { voter } = useCardano(); + const { voter } = useGetVoterInfo(); const { t } = useTranslation(); return ( app-logo { { /> - {t("footer.copyright")} + {t('footer.copyright')} diff --git a/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx b/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx index 9b5a6184f..41eaa2bb2 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx @@ -1,91 +1,44 @@ /* eslint-disable no-unsafe-optional-chaining */ -import { useMemo } from "react"; import { useNavigate, generatePath } from "react-router-dom"; -import { Box, CircularProgress } from "@mui/material"; +import { Box } from "@mui/material"; -import { Typography } from "@atoms"; -import { - useGetProposalsQuery, - useScreenDimension, - useTranslation, -} from "@hooks"; -import { GovernanceActionCard } from "@molecules"; -import { GOVERNANCE_ACTIONS_FILTERS, PATHS } from "@consts"; -import { useCardano } from "@context"; -import { getProposalTypeLabel, getFullGovActionId, openInNewTab } from "@utils"; -import { Slider } from "./Slider"; +import { Typography } from '@atoms'; +import { PATHS } from '@consts'; +import { useCardano } from '@context'; +import { useScreenDimension, useTranslation } from '@hooks'; +import { GovernanceActionCard } from '@molecules'; +import { getProposalTypeLabel, getFullGovActionId, openInNewTab } from '@utils'; +import { Slider } from './Slider'; type GovernanceActionsToVoteProps = { filters: string[]; + sorting: string; + proposals: { title: string; actions: ActionType[] }[]; onDashboard?: boolean; searchPhrase?: string; - sorting: string; }; -const defaultCategories = GOVERNANCE_ACTIONS_FILTERS.map( - (category) => category.key, -); - export const GovernanceActionsToVote = ({ filters, onDashboard = true, - searchPhrase = "", + proposals, + searchPhrase, sorting, }: GovernanceActionsToVoteProps) => { - const { voteTransaction } = useCardano(); + const { pendingTransaction } = useCardano(); const navigate = useNavigate(); const { isMobile } = useScreenDimension(); const { t } = useTranslation(); - const queryFilters = filters.length > 0 ? filters : defaultCategories; - - const { proposals, isProposalsLoading } = useGetProposalsQuery({ - filters: queryFilters, - sorting, - }); - - const groupedByType = (data?: ActionType[]) => - data?.reduce((groups, item) => { - const itemType = item.type; - - // TODO: Provide better typing for groups - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - if (!groups[itemType]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - groups[itemType] = { - title: itemType, - actions: [], - }; - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - groups[itemType].actions.push(item); - - return groups; - }, {}); - - const mappedData = useMemo(() => { - const groupedData = groupedByType( - proposals?.filter((i) => - getFullGovActionId(i.txHash, i.index) - .toLowerCase() - .includes(searchPhrase.toLowerCase()), - ), - ); - return Object.values(groupedData ?? []) as ToVoteDataType; - }, [proposals, searchPhrase]); - - return !isProposalsLoading ? ( + return ( <> - {!mappedData.length ? ( + {!proposals.length ? ( - {t("govActions.noResultsForTheSearch")} + {t('govActions.noResultsForTheSearch')} ) : ( <> - {mappedData?.map((item, index) => ( + {proposals?.map((item, index) => ( ( @@ -93,8 +46,8 @@ export const GovernanceActionsToVote = ({ className="keen-slider__slide" key={action.id} style={{ - overflow: "visible", - width: "auto", + overflow: 'visible', + width: 'auto', }} > onDashboard && - voteTransaction?.proposalId === + pendingTransaction.vote?.resourceId === action?.txHash + action?.index ? openInNewTab( - "https://adanordic.com/latest_transactions", - ) + "https://adanordic.com/latest_transactions", + ) : navigate( - onDashboard - ? generatePath( - PATHS.dashboardGovernanceActionsAction, - { - proposalId: getFullGovActionId( - action.txHash, - action.index, - ), - }, - ) - : PATHS.governanceActionsAction.replace( - ":proposalId", - getFullGovActionId( - action.txHash, - action.index, - ), + onDashboard + ? generatePath( + PATHS.dashboardGovernanceActionsAction, + { + proposalId: getFullGovActionId( + action.txHash, + action.index, ), - { - state: { ...action }, - }, - ) + }, + ) + : PATHS.governanceActionsAction.replace( + ":proposalId", + getFullGovActionId( + action.txHash, + action.index, + ), + ), + { + state: { ...action }, + }, + ) } /> @@ -149,7 +102,7 @@ export const GovernanceActionsToVote = ({ sorting={sorting} title={getProposalTypeLabel(item.title)} /> - {index < mappedData.length - 1 && ( + {index < proposals.length - 1 && ( )} @@ -157,15 +110,5 @@ export const GovernanceActionsToVote = ({ )} - ) : ( - - - ); }; diff --git a/govtool/frontend/src/components/organisms/RegisterAsSoleVoterBox.tsx b/govtool/frontend/src/components/organisms/RegisterAsSoleVoterBox.tsx index 8f68718fc..445b0a474 100644 --- a/govtool/frontend/src/components/organisms/RegisterAsSoleVoterBox.tsx +++ b/govtool/frontend/src/components/organisms/RegisterAsSoleVoterBox.tsx @@ -6,19 +6,17 @@ import { PATHS } from "@consts"; import { RegisterAsSoleVoterBoxContent } from "@organisms"; import { CenteredBoxBottomButtons } from "@molecules"; import { useCardano, useModal } from "@context"; +import { useGetVoterInfo } from "@/hooks"; export const RegisterAsSoleVoterBox = () => { const [isLoading, setIsLoading] = useState(false); - const { - buildSignSubmitConwayCertTx, - buildDRepRegCert, - buildDRepUpdateCert, - voter, - } = useCardano(); + const { buildSignSubmitConwayCertTx, buildDRepRegCert, buildDRepUpdateCert } = + useCardano(); const navigate = useNavigate(); const { openModal, closeModal } = useModal(); const { t } = useTranslation(); + const { voter } = useGetVoterInfo(); const onRegister = useCallback(async () => { setIsLoading(true); @@ -29,8 +27,7 @@ export const RegisterAsSoleVoterBox = () => { : await buildDRepRegCert(); const result = await buildSignSubmitConwayCertTx({ certBuilder, - type: "soleVoterRegistration", - registrationType: "registration", + type: "registerAsSoleVoter", }); if (result) { openModal({ diff --git a/govtool/frontend/src/components/organisms/RetireAsSoleVoterBox.tsx b/govtool/frontend/src/components/organisms/RetireAsSoleVoterBox.tsx index 500f26a1c..f691d3d72 100644 --- a/govtool/frontend/src/components/organisms/RetireAsSoleVoterBox.tsx +++ b/govtool/frontend/src/components/organisms/RetireAsSoleVoterBox.tsx @@ -6,6 +6,7 @@ import { PATHS } from "@consts"; import { CenteredBoxBottomButtons } from "@molecules"; import { useCardano, useModal } from "@context"; import { RetireAsSoleVoterBoxContent } from "@organisms"; +import { useGetVoterInfo } from "@/hooks"; export const RetireAsSoleVoterBox = () => { const [isLoading, setIsLoading] = useState(false); @@ -18,17 +19,23 @@ export const RetireAsSoleVoterBox = () => { } = useCardano(); const { openModal, closeModal } = useModal(); const { t } = useTranslation(); + const { voter } = useGetVoterInfo(); const onRetire = useCallback(async () => { try { setIsLoading(true); const isPendingTx = isPendingTransaction(); if (isPendingTx) return; - const certBuilder = await buildDRepRetirementCert(); + if (!voter?.deposit) { + throw new Error("Can not fetch deposit"); + } + const certBuilder = await buildDRepRetirementCert( + voter?.deposit?.toString() + ); const result = await buildSignSubmitConwayCertTx({ certBuilder, - type: "soleVoterRegistration", - registrationType: "retirement", + type: "retireAsSoleVoter", + voterDeposit: voter?.deposit?.toString(), }); if (result) { openModal({ @@ -73,6 +80,7 @@ export const RetireAsSoleVoterBox = () => { buildSignSubmitConwayCertTx, isPendingTransaction, openModal, + voter?.deposit, ]); return ( diff --git a/govtool/frontend/src/components/organisms/RetireAsSoleVoterBoxContent.tsx b/govtool/frontend/src/components/organisms/RetireAsSoleVoterBoxContent.tsx index bb371f0c4..983e0b81d 100644 --- a/govtool/frontend/src/components/organisms/RetireAsSoleVoterBoxContent.tsx +++ b/govtool/frontend/src/components/organisms/RetireAsSoleVoterBoxContent.tsx @@ -2,14 +2,13 @@ import { Link } from "@mui/material"; import { Trans } from "react-i18next"; import { Typography } from "@atoms"; -import { useScreenDimension, useTranslation } from "@hooks"; -import { correctAdaFormat, openInNewTab } from "@/utils"; -import { useCardano } from "@/context"; +import { useGetVoterInfo, useScreenDimension, useTranslation } from "@hooks"; +import { correctAdaFormat, openInNewTab } from "@utils"; export const RetireAsSoleVoterBoxContent = () => { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); - const { voter } = useCardano(); + const { voter } = useGetVoterInfo(); return ( <> diff --git a/govtool/frontend/src/components/organisms/Slider.tsx b/govtool/frontend/src/components/organisms/Slider.tsx index 6bc159e14..577ca56a6 100644 --- a/govtool/frontend/src/components/organisms/Slider.tsx +++ b/govtool/frontend/src/components/organisms/Slider.tsx @@ -38,7 +38,7 @@ export const Slider = ({ }: SliderProps) => { const { isMobile, screenWidth, pagePadding } = useScreenDimension(); const navigate = useNavigate(); - const { voteTransaction } = useCardano(); + const { pendingTransaction } = useCardano(); const { t } = useTranslation(); const DEFAULT_SLIDER_CONFIG = { @@ -75,7 +75,7 @@ export const Slider = ({ useEffect(() => { refresh(); - }, [filters, sorting, searchPhrase, voteTransaction?.proposalId, data]); + }, [filters, sorting, searchPhrase, pendingTransaction.vote?.resourceId, data]); const rangeSliderCalculationElement = dataLength < notSlicedDataLength diff --git a/govtool/frontend/src/context/getUtxos.ts b/govtool/frontend/src/context/getUtxos.ts new file mode 100644 index 000000000..23fc0a198 --- /dev/null +++ b/govtool/frontend/src/context/getUtxos.ts @@ -0,0 +1,83 @@ +import { CardanoApiWallet } from "@models"; +import * as Sentry from "@sentry/react"; +import { TransactionUnspentOutput } from "@emurgo/cardano-serialization-lib-asmjs"; +import { Buffer } from "buffer"; + +type Utxos = { + txid: unknown; + txindx: number; + amount: string; + str: string; + multiAssetStr: string; + TransactionUnspentOutput: TransactionUnspentOutput; +}[]; + +export const getUtxos = async ( + enabledApi: CardanoApiWallet +): Promise => { + const utxos = []; + + try { + const rawUtxos = await enabledApi.getUtxos(); + + // TODO maybe refactor + // eslint-disable-next-line no-restricted-syntax + for (const rawUtxo of rawUtxos) { + const utxo = TransactionUnspentOutput.from_bytes( + Buffer.from(rawUtxo, "hex") + ); + const input = utxo.input(); + const txid = input.transaction_id().to_hex(); + + const txindx = input.index(); + const output = utxo.output(); + const amount = output.amount().coin().to_str(); // ADA amount in lovelace + const multiasset = output.amount().multiasset(); + let multiAssetStr = ""; + + if (multiasset) { + const keys = multiasset.keys(); // policy Ids of thee multiasset + const N = keys.len(); + + for (let i = 0; i < N; i++) { + const policyId = keys.get(i); + const policyIdHex = policyId.to_hex(); + const assets = multiasset.get(policyId); + if (assets) { + const assetNames = assets.keys(); + const K = assetNames.len(); + + for (let j = 0; j < K; j++) { + const assetName = assetNames.get(j); + const assetNameString = assetName.name().toString(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const assetNameHex = Buffer.from( + assetName.name(), + "utf8" + ).toString("hex"); + const multiassetAmt = multiasset.get_asset(policyId, assetName); + multiAssetStr += `+ ${multiassetAmt.to_str()} + ${policyIdHex}.${assetNameHex} (${assetNameString})`; + } + } + } + } + + const obj = { + amount, + multiAssetStr, + str: `${txid} #${txindx} = ${amount}`, + TransactionUnspentOutput: utxo, + txid, + txindx, + }; + utxos.push(obj); + } + + return utxos; + } catch (err) { + Sentry.captureException(err); + // eslint-disable-next-line no-console + console.error(err); + } +}; diff --git a/govtool/frontend/src/context/pendingTransaction/index.ts b/govtool/frontend/src/context/pendingTransaction/index.ts new file mode 100644 index 000000000..b257e8518 --- /dev/null +++ b/govtool/frontend/src/context/pendingTransaction/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./usePendingTransaction"; +export * from "./utils"; diff --git a/govtool/frontend/src/context/pendingTransaction/types.ts b/govtool/frontend/src/context/pendingTransaction/types.ts new file mode 100644 index 000000000..f6aae4ade --- /dev/null +++ b/govtool/frontend/src/context/pendingTransaction/types.ts @@ -0,0 +1,41 @@ +export type TransactionTypeWithoutResource = + | "createGovAction" + | "registerAsDrep" + | "registerAsSoleVoter" + | "retireAsDrep" + | "retireAsSoleVoter" + | "updateMetaData"; + +export type TransactionTypeWithResource = "delegate" | "vote"; + +export type TransactionType = + | TransactionTypeWithoutResource + | TransactionTypeWithResource; + +export type TransactionStateWithoutResource = { + type: TransactionTypeWithoutResource; + transactionHash: string; + time: Date; + resourceId?: never; +}; + +export type TransactionStateWithResource = { + type: TransactionTypeWithResource; + transactionHash: string; + time: Date; + resourceId: string; +}; + +export type TransactionState = + | TransactionStateWithResource + | TransactionStateWithoutResource; + +export type PendingTransaction = + | Record< + TransactionTypeWithoutResource, + TransactionStateWithoutResource | null + > & + Record< + TransactionTypeWithResource, + TransactionStateWithResource | null + >; diff --git a/govtool/frontend/src/context/pendingTransaction/usePendingTransaction.ts b/govtool/frontend/src/context/pendingTransaction/usePendingTransaction.ts new file mode 100644 index 000000000..18f1e4a7c --- /dev/null +++ b/govtool/frontend/src/context/pendingTransaction/usePendingTransaction.ts @@ -0,0 +1,166 @@ +import { useCallback, useEffect, useState } from "react"; +import { + setItemToLocalStorage, + removeItemFromLocalStorage, + getItemFromLocalStorage, + PENDING_TRANSACTION_KEY, + wait, +} from "@utils"; +import { getTransactionStatus } from "@services"; +import { useTranslation } from "@hooks"; +import { StatusModalState } from "@organisms"; +import { useQueryClient } from "react-query"; +import { useModal, useSnackbar } from ".."; +import { TransactionState } from "./types"; +import { getDesiredResult, getQueryKey, refetchData } from "./utils"; + +const TIME_TO_EXPIRE_TRANSACTION = 3 * 60 * 1000; // 3 MINUTES +const TRANSACTION_REFRESH_TIME = 15 * 1000; // 15 SECONDS +const DB_SYNC_REFRESH_TIME = 3 * 1000; // 3 SECONDS +const DB_SYNC_MAX_ATTEMPTS = 10; + +type UsePendingTransactionProps = { + dRepID: string; + isEnabled: boolean; + stakeKey: string | undefined; +}; + +export const usePendingTransaction = ({ + isEnabled, + stakeKey, + dRepID, +}: UsePendingTransactionProps) => { + const { t } = useTranslation(); + const { openModal, closeModal } = useModal(); + const { addSuccessAlert, addWarningAlert, addErrorAlert } = useSnackbar(); + const queryClient = useQueryClient(); + + const [transaction, setTransaction] = useState(null); + + const pendingTransaction = { + delegate: transaction?.type === "delegate" ? transaction : null, + createGovAction: + transaction?.type === "createGovAction" ? transaction : null, + registerAsDrep: transaction?.type === "registerAsDrep" ? transaction : null, + registerAsSoleVoter: + transaction?.type === "registerAsSoleVoter" ? transaction : null, + retireAsDrep: transaction?.type === "retireAsDrep" ? transaction : null, + retireAsSoleVoter: + transaction?.type === "retireAsSoleVoter" ? transaction : null, + updateMetaData: transaction?.type === "updateMetaData" ? transaction : null, + vote: transaction?.type === "vote" ? transaction : null, + }; + + // Load transactions from local storage + useEffect(() => { + if (isEnabled) { + const fromLocalStorage = getItemFromLocalStorage( + `${PENDING_TRANSACTION_KEY}_${stakeKey}` + ); + setTransaction( + fromLocalStorage + ? { ...fromLocalStorage, time: new Date(fromLocalStorage.time) } + : null + ); + } + }, [isEnabled, stakeKey]); + + // Check transactions status + useEffect(() => { + if (!transaction?.transactionHash) return; + + const { transactionHash, type, resourceId } = transaction; + + const checkTransaction = async () => { + const status = await getTransactionStatus(transactionHash); + + const resetTransaction = () => { + removeItemFromLocalStorage(`${PENDING_TRANSACTION_KEY}_${stakeKey}`); + setTransaction(null); + }; + + if (status.transactionConfirmed) { + clearInterval(interval); + if (isEnabled) { + const desiredResult = getDesiredResult( + type, + dRepID, + resourceId, + stakeKey + ); + const queryKey = getQueryKey(type, transaction); + + let count = 0; + let isDBSyncUpdated = false; + while (!isDBSyncUpdated && count < DB_SYNC_MAX_ATTEMPTS) { + count++; + // eslint-disable-next-line no-await-in-loop + const data = await refetchData(type, queryClient, queryKey); + if (desiredResult === data) { + addSuccessAlert(t(`alerts.${type}.success`)); + resetTransaction(); + isDBSyncUpdated = true; + } else { + // eslint-disable-next-line no-await-in-loop + await wait(DB_SYNC_REFRESH_TIME); + } + } + + if (isTransactionExpired(transaction.time)) { + addErrorAlert(t(`alerts.${type}.failed`)); + resetTransaction(); + } + } + } + }; + + let interval = setInterval(checkTransaction, TRANSACTION_REFRESH_TIME); + checkTransaction(); + + if (isEnabled && transaction) { + addWarningAlert(t("alerts.transactionInProgress"), 10000); + } + }, [transaction]); + + const isPendingTransaction = useCallback(() => { + if (transaction) { + openModal({ + type: "statusModal", + state: { + status: "info", + title: t("modals.waitForTransaction.title"), + message: t("modals.waitForTransaction.message"), + buttonText: t("ok"), + onSubmit: () => { + closeModal(); + }, + dataTestId: "transaction-inprogress-modal", + }, + }); + return true; + } + return false; + }, [closeModal, openModal, transaction]); + + const updateTransaction = (data: Omit) => { + const newTransaction = { + time: new Date(), + ...data, + } as TransactionState; + + setTransaction(newTransaction); + setItemToLocalStorage( + `${PENDING_TRANSACTION_KEY}_${stakeKey}`, + JSON.stringify(newTransaction) + ); + }; + + return { + isPendingTransaction, + pendingTransaction, + updateTransaction, + }; +}; + +const isTransactionExpired = (time: Date): boolean => + Date.now() - time.getTime() > TIME_TO_EXPIRE_TRANSACTION; diff --git a/govtool/frontend/src/context/pendingTransaction/utils.tsx b/govtool/frontend/src/context/pendingTransaction/utils.tsx new file mode 100644 index 000000000..916a994ff --- /dev/null +++ b/govtool/frontend/src/context/pendingTransaction/utils.tsx @@ -0,0 +1,65 @@ +import { QueryClient, QueryKey } from "react-query"; +import { QUERY_KEYS } from "@/consts"; +import { TransactionType, TransactionState } from "./types"; + +export const getDesiredResult = ( + type: TransactionType, + dRepID: string, + resourceId: string | undefined, + stakeKey?: string +) => { + switch (type) { + case "delegate": { + // current delegation + if (resourceId === dRepID) return dRepID; + if (resourceId === "no confidence") return "drep_always_no_confidence"; + if (resourceId === "abstain") return "drep_always_abstain"; + return stakeKey; + } + case "registerAsDrep": + case "registerAsSoleVoter": + // is registered + return true; + case "retireAsDrep": + case "retireAsSoleVoter": + // is registered + return false; + default: + return undefined; + } +}; + +export const getQueryKey = ( + type: TransactionType, + transaction: TransactionState | null +) => { + switch (type) { + case "registerAsDrep": + case "retireAsDrep": + case "registerAsSoleVoter": + case "retireAsSoleVoter": + return [QUERY_KEYS.useGetDRepInfoKey, transaction]; + case "delegate": + return [QUERY_KEYS.getAdaHolderCurrentDelegationKey, transaction]; + default: + return undefined; + } +}; + +export const refetchData = async ( + type: TransactionType, + queryClient: QueryClient, + queryKey: QueryKey | undefined +) => { + if (queryKey === undefined) return; + + await queryClient.invalidateQueries(queryKey); + // TODO add better type for query data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = await queryClient.getQueryData(queryKey); + + if (type === "delegate") return data.currentDelegation; + if (type === "registerAsDrep" || type === "retireAsDrep") return data.isRegisteredAsDRep; + if (type === "registerAsSoleVoter" || type === "retireAsSoleVoter") return data.isRegisteredAsSoleVoter; + return undefined; +}; diff --git a/govtool/frontend/src/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index 53c0f2207..75eb48eb0 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -2,15 +2,12 @@ /* eslint-disable */ // @ts-nocheck import { - Dispatch, - SetStateAction, createContext, useCallback, useContext, - useEffect, useMemo, useState, -} from "react"; +} from 'react'; import { Address, BigNum, @@ -46,41 +43,37 @@ import { GovernanceAction, TreasuryWithdrawals, TreasuryWithdrawalsAction, -} from "@emurgo/cardano-serialization-lib-asmjs"; -import { Buffer } from "buffer"; -import { useNavigate } from "react-router-dom"; -import { Link } from "@mui/material"; -import * as Sentry from "@sentry/react"; -import { Trans } from "react-i18next"; - -import { PATHS } from "@consts"; -import { CardanoApiWallet, VoterInfo, Protocol } from "@models"; -import type { StatusModalState } from "@organisms"; +} from '@emurgo/cardano-serialization-lib-asmjs'; +import { Buffer } from 'buffer'; +import { useNavigate } from 'react-router-dom'; +import { Link } from '@mui/material'; +import * as Sentry from '@sentry/react'; +import { Trans } from 'react-i18next'; + +import { PATHS } from '@consts'; +import { CardanoApiWallet, Protocol } from '@models'; +import type { StatusModalState } from '@organisms'; import { checkIsMaintenanceOn, - DELEGATE_TO_KEY, - DELEGATE_TRANSACTION_KEY, generateAnchor, getItemFromLocalStorage, getPubDRepID, - GOVERNANCE_ACTION_KEY, openInNewTab, PROTOCOL_PARAMS_KEY, - REGISTER_SOLE_VOTER_TRANSACTION_KEY, - REGISTER_TRANSACTION_KEY, removeItemFromLocalStorage, SANCHO_INFO_KEY, setItemToLocalStorage, - VOTE_TRANSACTION_KEY, WALLET_LS_KEY, -} from "@utils"; -import { getEpochParams, getTransactionStatus } from "@services"; -import { useTranslation } from "@hooks"; +} from '@utils'; +import { getEpochParams } from '@services'; +import { useTranslation } from '@hooks'; +import { getUtxos } from './getUtxos'; +import { useModal, useSnackbar } from '.'; import { - setLimitedDelegationInterval, - setLimitedRegistrationInterval, -} from "./walletUtils"; -import { useModal, useSnackbar } from "."; + PendingTransaction, + TransactionType, + usePendingTransaction, +} from './pendingTransaction'; interface Props { children: React.ReactNode; @@ -91,15 +84,6 @@ interface EnableResponse { stakeKey?: boolean; error?: string; } -const TIME_TO_EXPIRE_TRANSACTION = 3 * 60 * 1000; // 3 MINUTES -const REFRESH_TIME = 15 * 1000; // 15 SECONDS - -type TransactionHistoryItem = { - transactionHash: string; - time?: Date; -}; - -export type DRepActionType = "retirement" | "registration" | "update" | ""; type InfoProps = { hash: string; @@ -119,61 +103,54 @@ interface CardanoContext { enable: (walletName: string) => Promise; isEnableLoading: string | null; error?: string; - voter: VoterInfo | undefined; isEnabled: boolean; pubDRepKey: string; dRepID: string; dRepIDBech32: string; isMainnet: boolean; stakeKey?: string; - setVoter: (key: undefined | VoterInfo) => void; setStakeKey: (key: string) => void; stakeKeys: string[]; walletApi?: CardanoApiWallet; - delegatedDRepID?: string; - setDelegatedDRepID: (key: string) => void; buildSignSubmitConwayCertTx: ({ certBuilder, - votingBuilder, govActionBuilder, + resourceId, type, - registrationType, + votingBuilder, + voterDeposit, }: { certBuilder?: CertificatesBuilder; - votingBuilder?: VotingBuilder; govActionBuilder?: VotingProposalBuilder; - type?: "delegation" | "registration" | "soleVoterRegistration" | "vote"; - proposalId?: string; - registrationType?: DRepActionType; + resourceId?: string; + type: TransactionType; + votingBuilder?: VotingBuilder; + voterDeposit?: string; }) => Promise; buildDRepRegCert: ( url?: string, hash?: string, + hash?: string, ) => Promise; buildVoteDelegationCert: (vote: string) => Promise; buildDRepUpdateCert: ( url?: string, hash?: string, + hash?: string, + ) => Promise; + buildDRepRetirementCert: ( + voterDeposit: string, ) => Promise; - buildDRepRetirementCert: () => Promise; buildVote: ( voteChoice: string, txHash: string, index: number, cip95MetadataURL?: string, cip95MetadataHash?: string, + cip95MetadataHash?: string, ) => Promise; - govActionTransaction: TransactionHistoryItem; - delegateTransaction: TransactionHistoryItem; - registerTransaction: TransactionHistoryItem & { type: DRepActionType }; - soleVoterTransaction: TransactionHistoryItem & { - type: Omit; - }; - delegateTo: string; - voteTransaction: TransactionHistoryItem & { proposalId: string }; + pendingTransaction: PendingTransaction; isPendingTransaction: () => boolean; - isDrepLoading: boolean; - setIsDrepLoading: Dispatch>; buildNewInfoGovernanceAction: ( infoProps: InfoProps, ) => Promise; @@ -194,19 +171,18 @@ type Utxos = { const NETWORK = import.meta.env.VITE_NETWORK_FLAG; const CardanoContext = createContext({} as CardanoContext); -CardanoContext.displayName = "CardanoContext"; +CardanoContext.displayName = 'CardanoContext'; const CardanoProvider = (props: Props) => { const [isEnabled, setIsEnabled] = useState(false); const [isEnableLoading, setIsEnableLoading] = useState(null); - const [voter, setVoter] = useState(undefined); const [walletApi, setWalletApi] = useState( undefined, ); const [address, setAddress] = useState(undefined); - const [pubDRepKey, setPubDRepKey] = useState(""); - const [dRepID, setDRepID] = useState(""); - const [dRepIDBech32, setDRepIDBech32] = useState(""); + const [pubDRepKey, setPubDRepKey] = useState(''); + const [dRepID, setDRepID] = useState(''); + const [dRepIDBech32, setDRepIDBech32] = useState(''); const [stakeKey, setStakeKey] = useState(undefined); const [stakeKeys, setStakeKeys] = useState([]); const [isMainnet, setIsMainnet] = useState(false); @@ -214,10 +190,6 @@ const CardanoProvider = (props: Props) => { const [registeredStakeKeysListState, setRegisteredPubStakeKeysState] = useState([]); const [error, setError] = useState(undefined); - const [delegatedDRepID, setDelegatedDRepID] = useState( - undefined, - ); - const [delegateTo, setDelegateTo] = useState(""); const [walletState, setWalletState] = useState<{ changeAddress: undefined | string; usedAddress: undefined | string; @@ -225,359 +197,22 @@ const CardanoProvider = (props: Props) => { changeAddress: undefined, usedAddress: undefined, }); - const [delegateTransaction, setDelegateTransaction] = - useState({ - time: undefined, - transactionHash: "", - }); - const [registerTransaction, setRegisterTransaction] = useState< - TransactionHistoryItem & { type: DRepActionType } - >({ time: undefined, transactionHash: "", type: "" }); - const [govActionTransaction, setGovActionTransaction] = - useState({ time: undefined, transactionHash: "" }); - const [soleVoterTransaction, setSoleVoterTransaction] = useState< - TransactionHistoryItem & { type: Omit } - >({ time: undefined, transactionHash: "", type: "" }); - const [voteTransaction, setVoteTransaction] = useState< - { proposalId: string } & TransactionHistoryItem - >({ time: undefined, transactionHash: "", proposalId: "" }); - const [isDrepLoading, setIsDrepLoading] = useState(true); - const { addSuccessAlert, addWarningAlert, addErrorAlert } = useSnackbar(); const { t } = useTranslation(); const epochParams = getItemFromLocalStorage(PROTOCOL_PARAMS_KEY); - const isPendingTransaction = useCallback(() => { - if ( - registerTransaction?.transactionHash || - soleVoterTransaction?.transactionHash || - delegateTransaction?.transactionHash || - voteTransaction?.transactionHash - ) { - openModal({ - type: "statusModal", - state: { - status: "info", - title: t("modals.waitForTransaction.title"), - message: t("modals.waitForTransaction.message"), - buttonText: t("ok"), - onSubmit: () => { - closeModal(); - }, - dataTestId: "transaction-inprogress-modal", - }, - }); - return true; - } - return false; - }, [ - closeModal, - delegateTransaction?.transactionHash, - openModal, - registerTransaction?.transactionHash, - soleVoterTransaction?.transactionHash, - voteTransaction?.transactionHash, - ]); - - useEffect(() => { - const delegateTransaction = JSON.parse( - getItemFromLocalStorage(`${DELEGATE_TRANSACTION_KEY}_${stakeKey}`), - ); - const registerTransaction = JSON.parse( - getItemFromLocalStorage(`${REGISTER_TRANSACTION_KEY}_${stakeKey}`), - ); - const soleVoterTransaction = JSON.parse( - getItemFromLocalStorage( - `${REGISTER_SOLE_VOTER_TRANSACTION_KEY}_${stakeKey}`, - ), - ); - const voteTransaction = JSON.parse( - getItemFromLocalStorage(`${VOTE_TRANSACTION_KEY}_${stakeKey}`), - ); - const delegateTo = getItemFromLocalStorage( - `${DELEGATE_TO_KEY}_${stakeKey}`, - ); - if (delegateTransaction?.transactionHash) { - setDelegateTransaction(delegateTransaction); - } - if (registerTransaction?.transactionHash) { - setRegisterTransaction(registerTransaction); - } - if (soleVoterTransaction?.transactionHash) { - setSoleVoterTransaction(soleVoterTransaction); - } - if (voteTransaction?.transactionHash) { - setVoteTransaction(voteTransaction); - } - if (delegateTo) { - setDelegateTo(delegateTo); - } - }, [isEnabled, stakeKey]); - - useEffect(() => { - if (delegateTransaction?.transactionHash) { - const checkDelegateTransaction = async () => { - const resetDelegateTransaction = () => { - clearInterval(interval); - removeItemFromLocalStorage(`${DELEGATE_TRANSACTION_KEY}_${stakeKey}`); - setDelegateTransaction({ - time: undefined, - transactionHash: "", - }); - }; - const status = await getTransactionStatus( - delegateTransaction.transactionHash, - ); - if (status.transactionConfirmed) { - if (isEnabled) { - await setLimitedDelegationInterval( - 3000, - 10, - dRepID, - delegateTo, - stakeKey, - ).then((isDelegated) => { - if (isDelegated) { - addSuccessAlert(t("alerts.delegation.success")); - } else { - addWarningAlert(t("alerts.delegation.refreshPage")); - } - }); - } - resetDelegateTransaction(); - } - if ( - new Date().getTime() - new Date(delegateTransaction?.time).getTime() > - TIME_TO_EXPIRE_TRANSACTION - ) { - resetDelegateTransaction(); - if (isEnabled) addErrorAlert(t("alerts.delegation.failed")); - } - }; - let interval = setInterval(checkDelegateTransaction, REFRESH_TIME); - checkDelegateTransaction(); - } - if (registerTransaction?.transactionHash) { - const checkRegisterTransaction = async () => { - const resetRegisterTransaction = () => { - clearInterval(interval); - removeItemFromLocalStorage(`${REGISTER_TRANSACTION_KEY}_${stakeKey}`); - setRegisterTransaction({ - time: undefined, - transactionHash: "", - type: "", - }); - }; - const status = await getTransactionStatus( - registerTransaction.transactionHash, - ); - if (status.transactionConfirmed) { - if (isEnabled) { - if ( - registerTransaction.type === "registration" || - registerTransaction.type === "retirement" - ) { - await setLimitedRegistrationInterval( - 3000, - 10, - dRepID, - registerTransaction.type, - setVoter, - ).then((isRegistered) => { - if (registerTransaction.type === "registration") { - if (isRegistered) { - addSuccessAlert(t("alerts.registration.success")); - } else { - addWarningAlert(t("alerts.registration.refreshPage")); - } - } else if (registerTransaction.type === "retirement") { - if (!isRegistered) { - addSuccessAlert(t("alerts.retirement.success")); - } else { - addWarningAlert(t("alerts.retirement.refreshPage")); - } - } - }); - } else { - addSuccessAlert(t("alerts.metadataUpdate.success")); - } - } - resetRegisterTransaction(); - } - if ( - new Date().getTime() - new Date(registerTransaction?.time).getTime() > - TIME_TO_EXPIRE_TRANSACTION - ) { - resetRegisterTransaction(); - if (isEnabled) { - addErrorAlert( - t( - `alerts.${ - registerTransaction.type === "retirement" - ? "retirement.failed" - : registerTransaction.type === "registration" - ? "registration.failed" - : "metadataUpdate.failed" - }`, - ), - ); - } - } - }; - let interval = setInterval(checkRegisterTransaction, REFRESH_TIME); - checkRegisterTransaction(); - } - if (soleVoterTransaction?.transactionHash) { - const checkRegisterTransaction = async () => { - const resetRegisterTransaction = () => { - clearInterval(interval); - removeItemFromLocalStorage( - `${REGISTER_SOLE_VOTER_TRANSACTION_KEY}_${stakeKey}`, - ); - setSoleVoterTransaction({ - time: undefined, - transactionHash: "", - type: "", - }); - }; - const status = await getTransactionStatus( - soleVoterTransaction.transactionHash, - ); - if (status.transactionConfirmed) { - if (isEnabled) { - await setLimitedRegistrationInterval( - 3000, - 10, - dRepID, - soleVoterTransaction.type, - setVoter, - ).then((isRegistered) => { - if (soleVoterTransaction.type === "registration") { - if (isRegistered) { - addSuccessAlert(t("alerts.soleVoterRegistration.success")); - } else { - addWarningAlert( - t("alerts.soleVoterRegistration.refreshPage"), - ); - } - } else if (soleVoterTransaction.type === "retirement") { - if (!isRegistered) { - addSuccessAlert(t("alerts.soleVoterRetirement.success")); - } else { - addWarningAlert(t("alerts.soleVoterRetirement.refreshPage")); - } - } - }); - } - resetRegisterTransaction(); - } - if ( - new Date().getTime() - - new Date(soleVoterTransaction?.time).getTime() > - TIME_TO_EXPIRE_TRANSACTION - ) { - resetRegisterTransaction(); - if (isEnabled) { - addErrorAlert( - t( - `alerts.${ - soleVoterTransaction.type === "retirement" - ? "retirement.failed" - : "registration.failed" - }`, - ), - ); - } - } - }; - let interval = setInterval(checkRegisterTransaction, REFRESH_TIME); - checkRegisterTransaction(); - } - if (voteTransaction?.transactionHash) { - const checkVoteTransaction = async () => { - const resetVoteTransaction = () => { - clearInterval(interval); - removeItemFromLocalStorage(`${VOTE_TRANSACTION_KEY}_${stakeKey}`); - setVoteTransaction({ - time: undefined, - transactionHash: "", - proposalId: "", - }); - }; - const status = await getTransactionStatus( - voteTransaction.transactionHash, - ); - if (status.transactionConfirmed) { - resetVoteTransaction(); - if (isEnabled) addSuccessAlert(t("alerts.voting.success")); - } - if ( - new Date().getTime() - new Date(voteTransaction?.time).getTime() > - TIME_TO_EXPIRE_TRANSACTION - ) { - resetVoteTransaction(); - if (isEnabled) addErrorAlert(t("alerts.voting.failed")); - } - }; - let interval = setInterval(checkVoteTransaction, REFRESH_TIME); - checkVoteTransaction(); - } - if (govActionTransaction?.transactionHash) { - const checkGovActionTransaction = async () => { - const resetGovActionTransaction = () => { - clearInterval(interval); - removeItemFromLocalStorage(`${GOVERNANCE_ACTION_KEY}_${stakeKey}`); - setGovActionTransaction({ - time: undefined, - transactionHash: "", - }); - }; - const status = await getTransactionStatus( - govActionTransaction.transactionHash, - ); - if (status.transactionConfirmed) { - resetGovActionTransaction(); - if (isEnabled) addSuccessAlert(t("alerts.govAction.success")); - } - if ( - new Date().getTime() - - new Date(govActionTransaction?.time).getTime() > - TIME_TO_EXPIRE_TRANSACTION - ) { - resetGovActionTransaction(); - if (isEnabled) addErrorAlert(t("alerts.govAction.failed")); - } - }; - let interval = setInterval(checkGovActionTransaction, REFRESH_TIME); - checkGovActionTransaction(); - } - if ( - isEnabled && - (voteTransaction?.transactionHash || - registerTransaction?.transactionHash || - soleVoterTransaction?.transactionHash || - delegateTransaction?.transactionHash || - govActionTransaction.transactionHash) - ) { - addWarningAlert(t("alerts.transactionInProgress"), 10000); - } - }, [ - delegateTransaction, - registerTransaction, - soleVoterTransaction, - voteTransaction, - govActionTransaction, - ]); + const { isPendingTransaction, updateTransaction, pendingTransaction } = + usePendingTransaction({ dRepID, isEnabled, stakeKey }); const getChangeAddress = async (enabledApi: CardanoApiWallet) => { try { const raw = await enabledApi.getChangeAddress(); const changeAddress = Address.from_bytes( - Buffer.from(raw, "hex"), + Buffer.from(raw, 'hex'), ).to_bech32(); setWalletState((prev) => ({ ...prev, changeAddress })); } catch (err) { Sentry.captureException(err); - console.log(err); + console.error(err); } }; @@ -586,85 +221,12 @@ const CardanoProvider = (props: Props) => { const raw = await enabledApi.getUsedAddresses(); const rawFirst = raw[0]; const usedAddress = Address.from_bytes( - Buffer.from(rawFirst, "hex"), + Buffer.from(rawFirst, 'hex'), ).to_bech32(); setWalletState((prev) => ({ ...prev, usedAddress })); } catch (err) { Sentry.captureException(err); - console.log(err); - } - }; - - const getUtxos = async ( - enabledApi: CardanoApiWallet, - ): Promise => { - const Utxos = []; - - try { - const rawUtxos = await enabledApi.getUtxos(); - - for (const rawUtxo of rawUtxos) { - const utxo = TransactionUnspentOutput.from_bytes( - Buffer.from(rawUtxo, "hex"), - ); - const input = utxo.input(); - const txid = Buffer.from( - input.transaction_id().to_bytes(), - "utf8", - ).toString("hex"); - const txindx = input.index(); - const output = utxo.output(); - const amount = output.amount().coin().to_str(); // ADA amount in lovelace - const multiasset = output.amount().multiasset(); - let multiAssetStr = ""; - - if (multiasset) { - const keys = multiasset.keys(); // policy Ids of thee multiasset - const N = keys.len(); - - for (let i = 0; i < N; i++) { - const policyId = keys.get(i); - const policyIdHex = Buffer.from( - policyId.to_bytes(), - "utf8", - ).toString("hex"); - const assets = multiasset.get(policyId); - if (assets) { - const assetNames = assets.keys(); - const K = assetNames.len(); - - for (let j = 0; j < K; j++) { - const assetName = assetNames.get(j); - const assetNameString = Buffer.from( - assetName.name(), - "utf8", - ).toString(); - const assetNameHex = Buffer.from( - assetName.name(), - "utf8", - ).toString("hex"); - const multiassetAmt = multiasset.get_asset(policyId, assetName); - multiAssetStr += `+ ${multiassetAmt.to_str()} + ${policyIdHex}.${assetNameHex} (${assetNameString})`; - } - } - } - } - - const obj = { - txid, - txindx, - amount, - str: `${txid} #${txindx} = ${amount}`, - multiAssetStr, - TransactionUnspentOutput: utxo, - }; - Utxos.push(obj); - } - - return Utxos; - } catch (err) { - Sentry.captureException(err); - console.log(err); + console.error(err); } }; @@ -678,13 +240,13 @@ const CardanoProvider = (props: Props) => { try { // Check that this wallet supports CIP-95 connection if (!window.cardano[walletName].supportedExtensions) { - throw new Error(t("errors.walletNoCIP30Support")); + throw new Error(t('errors.walletNoCIP30Support')); } else if ( !window.cardano[walletName].supportedExtensions.some( (item) => item.cip === 95, ) ) { - throw new Error(t("errors.walletNoCIP30Nor90Support")); + throw new Error(t('errors.walletNoCIP30Nor90Support')); } // Enable wallet connection const enabledApi = await window.cardano[walletName] @@ -702,14 +264,14 @@ const CardanoProvider = (props: Props) => { // Check if wallet has enabled the CIP-95 extension const enabledExtensions = await enabledApi.getExtensions(); if (!enabledExtensions.some((item) => item.cip === 95)) { - throw new Error(t("errors.walletNoCIP90FunctionsEnabled")); + throw new Error(t('errors.walletNoCIP90FunctionsEnabled')); } const network = await enabledApi.getNetworkId(); if (network != NETWORK) { throw new Error( - t("errors.tryingConnectTo", { - networkFrom: network == 1 ? "mainnet" : "testnet", - networkTo: network != 1 ? "mainnet" : "testnet", + t('errors.tryingConnectTo', { + networkFrom: network == 1 ? 'mainnet' : 'testnet', + networkTo: network != 1 ? 'mainnet' : 'testnet', }), ); } @@ -718,7 +280,7 @@ const CardanoProvider = (props: Props) => { const usedAddresses = await enabledApi.getUsedAddresses(); const unusedAddresses = await enabledApi.getUnusedAddresses(); if (!usedAddresses.length && !unusedAddresses.length) { - throw new Error(t("errors.noAddressesFound")); + throw new Error(t('errors.noAddressesFound')); } if (!usedAddresses.length) { setAddress(unusedAddresses[0]); @@ -748,7 +310,7 @@ const CardanoProvider = (props: Props) => { .to_hex(); }); } else { - console.warn(t("warnings.usingUnregisteredStakeKeys")); + console.warn(t('warnings.usingUnregisteredStakeKeys')); stakeKeysList = unregisteredStakeKeysList.map((stakeKey) => { const stakeKeyHash = PublicKey.from_hex(stakeKey).hash(); const stakeCredential = Credential.from_keyhash(stakeKeyHash); @@ -782,33 +344,33 @@ const CardanoProvider = (props: Props) => { stakeKeySet = true; } const dRepIDs = await getPubDRepID(enabledApi); - setPubDRepKey(dRepIDs?.dRepKey || ""); - setDRepID(dRepIDs?.dRepID || ""); - setDRepIDBech32(dRepIDs?.dRepIDBech32 || ""); + setPubDRepKey(dRepIDs?.dRepKey || ''); + setDRepID(dRepIDs?.dRepID || ''); + setDRepIDBech32(dRepIDs?.dRepIDBech32 || ''); setItemToLocalStorage(`${WALLET_LS_KEY}_name`, walletName); const protocol = await getEpochParams(); setItemToLocalStorage(PROTOCOL_PARAMS_KEY, protocol); - return { status: t("ok"), stakeKey: stakeKeySet }; + return { status: t('ok'), stakeKey: stakeKeySet }; } catch (e) { Sentry.captureException(e); console.error(e); setError(`${e}`); setAddress(undefined); setWalletApi(undefined); - setPubDRepKey(""); + setPubDRepKey(''); setStakeKey(undefined); setIsEnabled(false); throw { - status: "ERROR", - error: `${e == undefined ? t("errors.somethingWentWrong") : e}`, + status: 'ERROR', + error: `${e == undefined ? t('errors.somethingWentWrong') : e}`, }; } finally { setIsEnableLoading(null); } } - throw { status: "ERROR", error: t("errors.somethingWentWrong") }; + throw { status: 'ERROR', error: t('errors.somethingWentWrong') }; }, [isEnabled, stakeKeys], ); @@ -851,42 +413,40 @@ const CardanoProvider = (props: Props) => { } }, []); - const getTxUnspentOutputs = useCallback(async (utxos: Utxos) => { + const getTxUnspentOutputs = async (utxos: Utxos) => { const txOutputs = TransactionUnspentOutputs.new(); for (const utxo of utxos) { txOutputs.add(utxo.TransactionUnspentOutput); } return txOutputs; - }, []); + }; // Build, sign and submit transaction const buildSignSubmitConwayCertTx = useCallback( async ({ certBuilder, - votingBuilder, govActionBuilder, + resourceId, type, - proposalId, - registrationType, + votingBuilder, + voterDeposit, }: { certBuilder?: CertificatesBuilder; - votingBuilder?: VotingBuilder; govActionBuilder?: VotingProposalBuilder; - type?: "delegation" | "registration" | "soleVoterRegistration" | "vote"; - proposalId?: string; - registrationType?: DRepActionType; + resourceId?: string; + type: TransactionType; + votingBuilder?: VotingBuilder; + voterDeposit?: string; }) => { await checkIsMaintenanceOn(); const isPendingTx = isPendingTransaction(); if (isPendingTx) return; - console.log(walletState, "walletState"); - console.log(certBuilder, "certBuilder"); try { const txBuilder = await initTransactionBuilder(); if (!txBuilder) { - throw new Error(t("errors.appCannotCreateTransaction")); + throw new Error(t('errors.appCannotCreateTransaction')); } if (certBuilder) { @@ -915,12 +475,13 @@ const CardanoProvider = (props: Props) => { ); // Add output of 1 ADA to the address of our wallet - let outputValue = BigNum.from_str("1000000"); + let outputValue = BigNum.from_str('1000000'); - if (registrationType === "retirement" && voter?.deposit) { - outputValue = outputValue.checked_add( - BigNum.from_str(`${voter?.deposit}`), - ); + if ( + (type === 'retireAsDrep' || type === 'retireAsSoleVoter') && + voterDeposit + ) { + outputValue = outputValue.checked_add(BigNum.from_str(voterDeposit)); } txBuilder.add_output( @@ -930,7 +491,7 @@ const CardanoProvider = (props: Props) => { const utxos = await getUtxos(walletApi); if (!utxos) { - throw new Error(t("errors.appCannotGetUtxos")); + throw new Error(t('errors.appCannotGetUtxos')); } // Find the available UTXOs in the wallet and use them as Inputs for the transaction const txUnspentOutputs = await getTxUnspentOutputs(utxos); @@ -953,113 +514,35 @@ const CardanoProvider = (props: Props) => { // Ask wallet to to provide signature (witnesses) for the transaction let txVkeyWitnesses; - txVkeyWitnesses = await walletApi.signTx( - Buffer.from(tx.to_bytes(), "utf8").toString("hex"), - true, - ); + txVkeyWitnesses = await walletApi.signTx(tx.to_hex(), true); // Create witness set object using the witnesses provided by the wallet txVkeyWitnesses = TransactionWitnessSet.from_bytes( - Buffer.from(txVkeyWitnesses, "hex"), + Buffer.from(txVkeyWitnesses, 'hex'), ); - transactionWitnessSet.set_vkeys(txVkeyWitnesses.vkeys()); + const vkeys = txVkeyWitnesses.vkeys(); + + if (!vkeys) throw new Error(t('errors.appCannotGetVkeys')); + + transactionWitnessSet.set_vkeys(vkeys); // Build transaction with witnesses const signedTx = Transaction.new(tx.body(), transactionWitnessSet); - console.log( - Buffer.from(signedTx.to_bytes(), "utf8").toString("hex"), - "signed tx cbor", - ); // Submit built signed transaction to chain, via wallet's submit transaction endpoint - const result = await walletApi.submitTx( - Buffer.from(signedTx.to_bytes(), "utf8").toString("hex"), - ); + const result = await walletApi.submitTx(signedTx.to_hex()); // Set results so they can be rendered - const cip95ResultTx = Buffer.from(signedTx.to_bytes(), "utf8").toString( - "hex", - ); const resultHash = result; - const cip95ResultWitness = Buffer.from( - txVkeyWitnesses.to_bytes(), - "utf8", - ).toString("hex"); - if (govActionBuilder) { - setGovActionTransaction({ - time: new Date(), - transactionHash: resultHash, - }); - setItemToLocalStorage( - `${GOVERNANCE_ACTION_KEY}_${stakeKey}`, - JSON.stringify({ - time: new Date(), - transactionHash: resultHash, - }), - ); - } - if (type === "registration") { - setRegisterTransaction({ - time: new Date(), - transactionHash: resultHash, - type: registrationType ?? "", - }); - setItemToLocalStorage( - `${REGISTER_TRANSACTION_KEY}_${stakeKey}`, - JSON.stringify({ - time: new Date(), - transactionHash: resultHash, - type: registrationType, - }), - ); - } - if (type === "soleVoterRegistration" && registrationType !== "update") { - setSoleVoterTransaction({ - time: new Date(), - transactionHash: resultHash, - type: registrationType ?? "", - }); - setItemToLocalStorage( - `${REGISTER_SOLE_VOTER_TRANSACTION_KEY}_${stakeKey}`, - JSON.stringify({ - time: new Date(), - transactionHash: resultHash, - type: registrationType, - }), - ); - } - if (type === "delegation") { - setDelegateTransaction({ - time: new Date(), - transactionHash: resultHash, - }); - setItemToLocalStorage( - `${DELEGATE_TRANSACTION_KEY}_${stakeKey}`, - JSON.stringify({ - time: new Date(), - transactionHash: resultHash, - }), - ); - } - if (type === "vote") { - setVoteTransaction({ - time: new Date(), - transactionHash: resultHash, - proposalId: proposalId ?? "", - }); - setItemToLocalStorage( - `${VOTE_TRANSACTION_KEY}_${stakeKey}`, - JSON.stringify({ - time: new Date(), - transactionHash: resultHash, - proposalId: proposalId ?? "", - }), - ); - } - console.log(cip95ResultTx, "cip95ResultTx"); - console.log(resultHash, "cip95ResultHash"); - console.log(cip95ResultWitness, "cip95ResultWitness"); + updateTransaction({ + transactionHash: resultHash, + type, + resourceId, + }); + + console.log(signedTx.to_hex(), 'signed tx cbor'); return resultHash; - } catch (error) { + // TODO: type error + } catch (error: any) { const walletName = getItemFromLocalStorage(`${WALLET_LS_KEY}_name`); const isWalletConnected = await window.cardano[walletName].isEnabled(); @@ -1068,22 +551,11 @@ const CardanoProvider = (props: Props) => { } Sentry.captureException(error); - console.log(error, "error"); + console.error(error, 'error'); throw error?.info ?? error; } }, - [ - walletState, - walletApi, - getUtxos, - registerTransaction.transactionHash, - soleVoterTransaction.transactionHash, - delegateTransaction.transactionHash, - voteTransaction.transactionHash, - stakeKey, - isPendingTransaction, - voter, - ], + [isPendingTransaction, stakeKey, updateTransaction, walletApi, walletState], ); const buildVoteDelegationCert = useCallback( @@ -1093,7 +565,7 @@ const CardanoProvider = (props: Props) => { const certBuilder = CertificatesBuilder.new(); let stakeCred; if (!stakeKey) { - throw new Error(t("errors.noStakeKeySelected")); + throw new Error(t('errors.noStakeKeySelected')); } // Remove network tag from stake key hash const stakeKeyHash = Ed25519KeyHash.from_hex(stakeKey.substring(2)); @@ -1101,18 +573,18 @@ const CardanoProvider = (props: Props) => { if (registeredStakeKeysListState.length > 0) { stakeCred = Credential.from_keyhash(stakeKeyHash); } else { - console.log(t("errors.registeringStakeKey")); stakeCred = Credential.from_keyhash(stakeKeyHash); const stakeKeyRegCert = StakeRegistration.new(stakeCred); certBuilder.add(Certificate.new_stake_registration(stakeKeyRegCert)); } + // Create correct DRep let targetDRep; - if (target === "abstain") { + if (target === 'abstain') { targetDRep = DRep.new_always_abstain(); - } else if (target === "no confidence") { + } else if (target === 'no confidence') { targetDRep = DRep.new_always_no_confidence(); - } else if (target.includes("drep")) { + } else if (target.includes('drep')) { targetDRep = DRep.new_key_hash(Ed25519KeyHash.from_bech32(target)); } else { targetDRep = DRep.new_key_hash(Ed25519KeyHash.from_hex(target)); @@ -1120,10 +592,8 @@ const CardanoProvider = (props: Props) => { // Create cert object const voteDelegationCert = VoteDelegation.new(stakeCred, targetDRep); // add cert to tbuilder - certBuilder.add(Certificate.new_vote_delegation(voteDelegationCert)); - setDelegateTo(target); - setItemToLocalStorage(`${DELEGATE_TO_KEY}_${stakeKey}`, target); + return certBuilder; } catch (e) { Sentry.captureException(e); @@ -1159,7 +629,7 @@ const CardanoProvider = (props: Props) => { anchor, ); } else { - console.log(t("errors.notUsingAnchor")); + console.log(t('errors.notUsingAnchor')); dRepRegCert = DrepRegistration.new( dRepCred, BigNum.from_str(`${epochParams.drep_deposit}`), @@ -1170,7 +640,7 @@ const CardanoProvider = (props: Props) => { return certBuilder; } catch (e) { Sentry.captureException(e); - console.log(e); + console.error(e); throw e; } }, @@ -1213,8 +683,8 @@ const CardanoProvider = (props: Props) => { ); // conway alpha - const buildDRepRetirementCert = - useCallback(async (): Promise => { + const buildDRepRetirementCert = useCallback( + async (voterDeposit: string): Promise => { try { // Build DRep Registration Certificate const certBuilder = CertificatesBuilder.new(); @@ -1224,7 +694,7 @@ const CardanoProvider = (props: Props) => { const dRepRetirementCert = DrepDeregistration.new( dRepCred, - BigNum.from_str(`${voter?.deposit}`), + BigNum.from_str(voterDeposit), ); // add cert to tbuilder certBuilder.add( @@ -1236,7 +706,9 @@ const CardanoProvider = (props: Props) => { console.log(e); throw e; } - }, [dRepID, voter]); + }, + [dRepID], + ); const buildVote = useCallback( async ( @@ -1258,9 +730,9 @@ const CardanoProvider = (props: Props) => { ); let votingChoice; - if (voteChoice === "yes") { + if (voteChoice === 'yes') { votingChoice = 1; - } else if (voteChoice === "no") { + } else if (voteChoice === 'no') { votingChoice = 0; } else { votingChoice = 2; @@ -1294,11 +766,11 @@ const CardanoProvider = (props: Props) => { const getRewardAddress = useCallback(async () => { const addresses = await walletApi?.getRewardAddresses(); if (!addresses) { - throw new Error("Can not get reward addresses from wallet."); + throw new Error('Can not get reward addresses from wallet.'); } const firstAddress = addresses[0]; const bech32Address = Address.from_bytes( - Buffer.from(firstAddress, "hex"), + Buffer.from(firstAddress, 'hex'), ).to_bech32(); return RewardAddress.from_address(Address.from_bech32(bech32Address)); @@ -1316,7 +788,7 @@ const CardanoProvider = (props: Props) => { const anchor = generateAnchor(url, hash); const rewardAddr = await getRewardAddress(); - if (!rewardAddr) throw new Error("Can not get reward address"); + if (!rewardAddr) throw new Error('Can not get reward address'); // Create voting proposal const votingProposal = VotingProposal.new( @@ -1344,7 +816,7 @@ const CardanoProvider = (props: Props) => { Address.from_bech32(receivingAddress), ); - if (!treasuryTarget) throw new Error("Can not get tresasury target"); + if (!treasuryTarget) throw new Error('Can not get tresasury target'); const myWithdrawal = BigNum.from_str(amount); const withdrawals = TreasuryWithdrawals.new(); @@ -1358,7 +830,7 @@ const CardanoProvider = (props: Props) => { const rewardAddr = await getRewardAddress(); - if (!rewardAddr) throw new Error("Can not get reward address"); + if (!rewardAddr) throw new Error('Can not get reward address'); // Create voting proposal const votingProposal = VotingProposal.new( treasuryGovAct, @@ -1387,32 +859,21 @@ const CardanoProvider = (props: Props) => { buildTreasuryGovernanceAction, buildVote, buildVoteDelegationCert, - delegatedDRepID, - delegateTo, - delegateTransaction, disconnectWallet, dRepID, dRepIDBech32, enable, error, - isDrepLoading, isEnabled, isEnableLoading, isMainnet, isPendingTransaction, + pendingTransaction, pubDRepKey, - registerTransaction, - setDelegatedDRepID, - setIsDrepLoading, setStakeKey, - setVoter, - soleVoterTransaction, stakeKey, stakeKeys, - voter, - voteTransaction, walletApi, - govActionTransaction, }), [ address, @@ -1424,32 +885,21 @@ const CardanoProvider = (props: Props) => { buildTreasuryGovernanceAction, buildVote, buildVoteDelegationCert, - delegatedDRepID, - delegateTo, - delegateTransaction, disconnectWallet, dRepID, dRepIDBech32, enable, error, - isDrepLoading, isEnabled, isEnableLoading, isMainnet, isPendingTransaction, + pendingTransaction, pubDRepKey, - registerTransaction, - setDelegatedDRepID, - setIsDrepLoading, setStakeKey, - setVoter, - soleVoterTransaction, stakeKey, stakeKeys, - voter, - voteTransaction, walletApi, - govActionTransaction, ], ); @@ -1464,7 +914,7 @@ function useCardano() { const { t } = useTranslation(); if (context === undefined) { - throw new Error(t("errors.useCardano")); + throw new Error(t('errors.useCardano')); } const enable = useCallback( @@ -1477,22 +927,22 @@ function useCardano() { if (!result.error) { closeModal(); if (result.stakeKey) { - addSuccessAlert(t("alerts.walletConnected"), 3000); + addSuccessAlert(t('alerts.walletConnected'), 3000); } if (!isSanchoInfoShown) { openModal({ - type: "statusModal", + type: 'statusModal', state: { - status: "info", - dataTestId: "info-about-sancho-net-modal", + status: 'info', + dataTestId: 'info-about-sancho-net-modal', message: (

- {t("system.sanchoNetIsBeta")} + {t('system.sanchoNetIsBeta')} openInNewTab("https://sancho.network/")} - sx={{ cursor: "pointer" }} + onClick={() => openInNewTab('https://sancho.network/')} + sx={{ cursor: 'pointer' }} > - {t("system.sanchoNet")} + {t('system.sanchoNet')} .
@@ -1505,8 +955,8 @@ function useCardano() { />

), - title: t("system.toolConnectedToSanchonet"), - buttonText: t("ok"), + title: t('system.toolConnectedToSanchonet'), + buttonText: t('ok'), }, }); setItemToLocalStorage(`${SANCHO_INFO_KEY}_${walletName}`, true); @@ -1518,15 +968,15 @@ function useCardano() { await context.disconnectWallet(); navigate(PATHS.home); openModal({ - type: "statusModal", + type: 'statusModal', state: { - status: "warning", - message: e?.error?.replace("Error: ", ""), + status: 'warning', + message: e?.error?.replace('Error: ', ''), onSubmit: () => { closeModal(); }, - title: t("modals.common.oops"), - dataTestId: "wallet-connection-error-modal", + title: t('modals.common.oops'), + dataTestId: 'wallet-connection-error-modal', }, }); throw e; diff --git a/govtool/frontend/src/context/walletUtils.ts b/govtool/frontend/src/context/walletUtils.ts deleted file mode 100644 index 6b3258458..000000000 --- a/govtool/frontend/src/context/walletUtils.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { getAdaHolderCurrentDelegation, getVoterInfo } from "@services"; -import { VoterInfo } from "@models"; -import { DRepActionType } from "./wallet"; - -export const setLimitedRegistrationInterval = ( - intervalTime: number, - attemptsNumber: number, - dRepID: string, - transactionType: DRepActionType | Omit, - setVoter: (key: undefined | VoterInfo) => void, -): Promise => - new Promise((resolve) => { - const desiredResult = transactionType === "registration"; - let count = 0; - - const interval = setInterval(async () => { - if (count < attemptsNumber) { - // TODO: Refactor this to not iterate over the same data - count++; - - try { - const data = await getVoterInfo(dRepID); - - if ( - data.isRegisteredAsDRep === desiredResult || - data.isRegisteredAsSoleVoter === desiredResult - ) { - setVoter(data); - clearInterval(interval); - resolve(desiredResult); - } - } catch (error) { - clearInterval(interval); - resolve(!desiredResult); - } - } else { - clearInterval(interval); - resolve(!desiredResult); - } - }, intervalTime); - }); - -export const setLimitedDelegationInterval = ( - intervalTime: number, - attemptsNumber: number, - dRepID: string, - delegateTo: string, - stakeKey?: string, -): Promise => - new Promise((resolve) => { - let count = 0; - - const interval = setInterval(async () => { - if (count < attemptsNumber) { - count++; - - try { - const currentDelegation = await getAdaHolderCurrentDelegation({ - stakeKey, - }); - - if ( - (delegateTo === dRepID && currentDelegation === dRepID) || - (delegateTo === "no confidence" && - currentDelegation === "drep_always_no_confidence") || - (delegateTo === "abstain" && - currentDelegation === "drep_always_abstain") || - (delegateTo !== dRepID && delegateTo === currentDelegation) - ) { - clearInterval(interval); - resolve(true); - } - } catch (error) { - clearInterval(interval); - resolve(false); - } - } else { - clearInterval(interval); - resolve(false); - } - }, intervalTime); - }); diff --git a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts index 70c9db079..22843aba7 100644 --- a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts +++ b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts @@ -14,17 +14,17 @@ import { PATHS, storageInformationErrorModals, } from "@consts"; -import { - GovernanceActionFieldSchemas, - GovernanceActionType, -} from "@/types/governanceAction"; +import { useCardano, useModal } from "@context"; import { canonizeJSON, downloadJson, generateJsonld, validateMetadataHash, -} from "@/utils"; -import { useCardano, useModal } from "@/context"; +} from "@utils"; +import { + GovernanceActionFieldSchemas, + GovernanceActionType, +} from "@/types/governanceAction"; export type CreateGovernanceActionValues = { links?: { link: string }[]; @@ -135,7 +135,7 @@ export const useCreateGovernanceActionForm = ( type: "statusModal", state: { ...storageInformationErrorModals[ - error.message as MetadataHashValidationErrors + error.message as MetadataHashValidationErrors ], onSubmit: backToForm, onCancel: backToDashboard, @@ -216,7 +216,10 @@ export const useCreateGovernanceActionForm = ( await validateHash(data.storingURL, hash); const govActionBuilder = await buildTransaction(data); - await buildSignSubmitConwayCertTx({ govActionBuilder }); + await buildSignSubmitConwayCertTx({ + govActionBuilder, + type: "createGovAction", + }); showSuccessModal(); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/govtool/frontend/src/hooks/forms/useDelegateTodRepForm.tsx b/govtool/frontend/src/hooks/forms/useDelegateTodRepForm.tsx index f2fdc75f5..12794fd79 100644 --- a/govtool/frontend/src/hooks/forms/useDelegateTodRepForm.tsx +++ b/govtool/frontend/src/hooks/forms/useDelegateTodRepForm.tsx @@ -12,11 +12,7 @@ export interface DelegateTodrepFormValues { } export const useDelegateTodRepForm = () => { - const { - setDelegatedDRepID, - buildSignSubmitConwayCertTx, - buildVoteDelegationCert, - } = useCardano(); + const { buildSignSubmitConwayCertTx, buildVoteDelegationCert } = useCardano(); const { data: drepList } = useGetDRepListQuery(); const { openModal, closeModal, modal } = useModal(); const [isLoading, setIsLoading] = useState(false); @@ -40,7 +36,6 @@ export const useDelegateTodRepForm = () => { async ({ dRepID }: DelegateTodrepFormValues) => { setIsLoading(true); try { - setDelegatedDRepID(dRepID); let isValidDrep = false; if (drepList?.length) { isValidDrep = drepList.some( @@ -54,7 +49,8 @@ export const useDelegateTodRepForm = () => { const certBuilder = await buildVoteDelegationCert(dRepID); const result = await buildSignSubmitConwayCertTx({ certBuilder, - type: "delegation", + type: "delegate", + resourceId: dRepID, }); if (result) { openModal({ diff --git a/govtool/frontend/src/hooks/forms/useUpdatedRepMetadataForm.tsx b/govtool/frontend/src/hooks/forms/useUpdatedRepMetadataForm.tsx index d2217c9f4..bd81d6318 100644 --- a/govtool/frontend/src/hooks/forms/useUpdatedRepMetadataForm.tsx +++ b/govtool/frontend/src/hooks/forms/useUpdatedRepMetadataForm.tsx @@ -41,13 +41,12 @@ export const useUpdatedRepMetadataForm = () => { ); const result = await buildSignSubmitConwayCertTx({ certBuilder, - type: "registration", - registrationType: "update", + type: "updateMetaData", }); - if (result) addSuccessAlert(t("alerts.metadataUpdate.success")); + if (result) addSuccessAlert(t("alerts.updateMetaData.success")); navigate(PATHS.dashboard); } catch (e) { - addErrorAlert(t("alerts.metadataUpdate.failed")); + addErrorAlert(t("alerts.updateMetaData.failed")); } finally { setIsLoading(false); } diff --git a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx index 6714b33bd..107dc7cfd 100644 --- a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx @@ -98,7 +98,7 @@ export const useVoteActionForm = () => { const result = await buildSignSubmitConwayCertTx({ votingBuilder, type: "vote", - proposalId: state.txHash + state.index, + resourceId: state.txHash + state.index, }); if (result) { addSuccessAlert("Vote submitted"); diff --git a/govtool/frontend/src/hooks/queries/useGetAdaHolderCurrentDelegationQuery.ts b/govtool/frontend/src/hooks/queries/useGetAdaHolderCurrentDelegationQuery.ts index 152a0c166..94dc2621d 100644 --- a/govtool/frontend/src/hooks/queries/useGetAdaHolderCurrentDelegationQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetAdaHolderCurrentDelegationQuery.ts @@ -5,12 +5,12 @@ import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; export const useGetAdaHolderCurrentDelegationQuery = (stakeKey?: string) => { - const { delegateTransaction } = useCardano(); + const { pendingTransaction } = useCardano(); const { data, isLoading } = useQuery({ queryKey: [ QUERY_KEYS.getAdaHolderCurrentDelegationKey, - delegateTransaction.transactionHash, + pendingTransaction.delegate, ], queryFn: () => getAdaHolderCurrentDelegation({ stakeKey }), enabled: !!stakeKey, diff --git a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts index f1471eab0..4e9aa0c26 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts @@ -5,12 +5,15 @@ import { useCardano } from "@context"; import { getDRepList } from "@services"; export const useGetDRepListQuery = () => { - const { registerTransaction } = useCardano(); + const { pendingTransaction } = useCardano(); const { data, isLoading } = useQuery({ queryKey: [ QUERY_KEYS.useGetDRepListKey, - registerTransaction?.transactionHash, + pendingTransaction.registerAsSoleVoter || + pendingTransaction.registerAsDrep || + pendingTransaction.retireAsSoleVoter || + pendingTransaction.retireAsDrep, ], queryFn: getDRepList, }); diff --git a/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts index dbc306d3f..034423c29 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts @@ -6,12 +6,12 @@ import { getDRepVotes } from "@services"; import { VotedProposal } from "@/models/api"; export const useGetDRepVotesQuery = (filters: string[], sorting: string) => { - const { dRepID, voteTransaction } = useCardano(); + const { dRepID, pendingTransaction } = useCardano(); const { data, isLoading, refetch, isRefetching } = useQuery({ queryKey: [ QUERY_KEYS.useGetDRepVotesKey, - voteTransaction.transactionHash, + pendingTransaction.vote, filters, sorting, ], diff --git a/govtool/frontend/src/hooks/queries/useGetDRepVotingPowerQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepVotingPowerQuery.ts index 22185f4b7..03247b518 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepVotingPowerQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepVotingPowerQuery.ts @@ -3,9 +3,11 @@ import { useQuery } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; import { getDRepVotingPower } from "@services"; +import { useGetVoterInfo } from "."; export const useGetDRepVotingPowerQuery = () => { - const { dRepID, voter } = useCardano(); + const { dRepID } = useCardano(); + const { voter } = useGetVoterInfo(); const { data, isLoading } = useQuery({ queryKey: [ diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts index 44cb3ea25..90e2ac016 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts @@ -9,7 +9,7 @@ export const useGetProposalsInfiniteQuery = ({ pageSize = 10, sorting = "", }: getProposalsArguments) => { - const { dRepID, isEnabled, voteTransaction } = useCardano(); + const { dRepID, isEnabled, pendingTransaction } = useCardano(); const fetchProposals = ({ pageParam = 0 }) => getProposals({ @@ -30,11 +30,11 @@ export const useGetProposalsInfiniteQuery = ({ } = useInfiniteQuery( [ QUERY_KEYS.useGetProposalsInfiniteKey, + dRepID, filters, - sorting, - voteTransaction.proposalId, isEnabled, - dRepID, + pendingTransaction.vote, + sorting, ], fetchProposals, { diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index 0500b33d5..3d0434acd 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -3,12 +3,14 @@ import { useQuery } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; import { getProposals, getProposalsArguments } from "@services"; +import { getFullGovActionId } from "@utils"; export const useGetProposalsQuery = ({ filters = [], sorting, + searchPhrase, }: getProposalsArguments) => { - const { dRepID, isEnabled, voteTransaction } = useCardano(); + const { dRepID, pendingTransaction } = useCardano(); const fetchProposals = async (): Promise => { const allProposals = await Promise.all( @@ -25,15 +27,44 @@ export const useGetProposalsQuery = ({ QUERY_KEYS.useGetProposalsKey, filters, sorting, - voteTransaction.proposalId, - isEnabled, dRepID, + pendingTransaction.vote, ], fetchProposals, ); + const mappedData = Object.values( + (groupedByType( + data?.filter((i) => + getFullGovActionId(i.txHash, i.index) + .toLowerCase() + .includes(searchPhrase.toLowerCase())) + ) ?? []) as ToVoteDataType + ); + return { isProposalsLoading: isLoading, - proposals: data, + proposals: mappedData, }; }; + +const groupedByType = (data?: ActionType[]) => data?.reduce((groups, item) => { + const itemType = item.type; + + // TODO: Provide better typing for groups + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + if (!groups[itemType]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + groups[itemType] = { + title: itemType, + actions: [], + }; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + groups[itemType].actions.push(item); + + return groups; +}, {}); diff --git a/govtool/frontend/src/hooks/queries/useGetVoterInfoQuery.ts b/govtool/frontend/src/hooks/queries/useGetVoterInfoQuery.ts index 6ec175541..5eab75e42 100644 --- a/govtool/frontend/src/hooks/queries/useGetVoterInfoQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetVoterInfoQuery.ts @@ -5,17 +5,19 @@ import { useCardano } from "@context"; import { getVoterInfo } from "@services"; export const useGetVoterInfo = () => { - const { dRepID, registerTransaction, soleVoterTransaction } = useCardano(); + const { dRepID, pendingTransaction } = useCardano(); - const { data, isLoading } = useQuery({ + const { data } = useQuery({ queryKey: [ QUERY_KEYS.useGetDRepInfoKey, - registerTransaction?.transactionHash, - soleVoterTransaction?.transactionHash, + pendingTransaction.registerAsDrep || + pendingTransaction.registerAsSoleVoter || + pendingTransaction.retireAsDrep || + pendingTransaction.retireAsSoleVoter, ], enabled: !!dRepID, queryFn: () => getVoterInfo(dRepID), }); - return { data, isLoading }; + return { voter: data }; }; diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index 39e37f929..f70098f2a 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -1,43 +1,45 @@ export const en = { translation: { alerts: { - delegation: { + delegate: { failed: "Delegation transaction failed", - refreshPage: + warning: "Your voting power has been successfully delegated! Please refresh the page.", success: "Your voting power has been successfully delegated!", }, - govAction: { + createGovAction: { failed: "Creating Governance Action transaction failed", success: "Your Governance Action has been submitted", }, - metadataUpdate: { - failed: "Update DRep metadata transaction failed", - success: "You have successfully updated DRep metadata!", - }, - registration: { + registerAsDrep: { failed: "Registration transaction failed", - refreshPage: + warning: "You have successfully registered as a DRep! Please refresh the page.", success: "You have successfully registered as a DRep!", }, - retirement: { + registerAsSoleVoter: { + failed: "Registration transaction failed", + warning: + "You have successfully registered as a Sole Voter! Please refresh the page.", + success: "You have successfully registered as a Sole Voter!", + }, + retireAsDrep: { failed: "Retirement transaction failed", - refreshPage: + warning: "You have successfully retired from being a DRep! Please refresh the page.", success: "You have successfully retired from being a DRep!", }, - soleVoterRegistration: { - refreshPage: - "You have successfully registered as a Sole Voter! Please refresh the page.", - success: "You have successfully registered as a Sole Voter!", - }, - soleVoterRetirement: { - refreshPage: + retireAsSoleVoter: { + failed: "Retirement transaction failed", + warning: "You have successfully retired from being a Sole Voter! Please refresh the page.", success: "You have successfully retired from being a SoleVoter!", }, - voting: { + updateMetaData: { + failed: "Update DRep metadata transaction failed", + success: "You have successfully updated DRep metadata!", + }, + vote: { failed: "Vote transaction failed", success: "You have successfully voted!", }, @@ -259,6 +261,7 @@ export const en = { errors: { appCannotCreateTransaction: "Application can not create transaction.", appCannotGetUtxos: "Application can not get utxos", + appCannotGetVkeys: "Application can not get vkey", checkIsWalletConnected: "Check if the wallet is connected.", dRepIdNotFound: "DrepId not found", invalidGovernanceActionType: "Invalid Governance Action Type", diff --git a/govtool/frontend/src/main.tsx b/govtool/frontend/src/main.tsx index 14d080e63..6243d91d8 100644 --- a/govtool/frontend/src/main.tsx +++ b/govtool/frontend/src/main.tsx @@ -8,6 +8,7 @@ import { useNavigationType, } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "react-query"; +import { ReactQueryDevtools } from "react-query/devtools"; import TagManager from "react-gtm-module"; import { ThemeProvider } from "@emotion/react"; import * as Sentry from "@sentry/react"; @@ -89,6 +90,9 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + {import.meta.env.VITE_IS_DEV && ( + + )} , ); diff --git a/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx b/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx index d54896e55..8685bfbf8 100644 --- a/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx +++ b/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx @@ -20,6 +20,7 @@ import { DataActionsBar, GovernanceActionCard } from "@molecules"; import { useFetchNextPageDetector, useGetProposalsInfiniteQuery, + useGetVoterInfo, useSaveScrollPosition, useScreenDimension, useTranslation, @@ -38,7 +39,8 @@ export const DashboardGovernanceActionsCategory = () => { const [chosenSorting, setChosenSorting] = useState(""); const { isMobile, screenWidth } = useScreenDimension(); const navigate = useNavigate(); - const { voter, isDrepLoading, voteTransaction } = useCardano(); + const { pendingTransaction, isEnableLoading } = useCardano(); + const { voter } = useGetVoterInfo(); const { t } = useTranslation(); const { @@ -51,6 +53,7 @@ export const DashboardGovernanceActionsCategory = () => { } = useGetProposalsInfiniteQuery({ filters: [category?.replace(/ /g, "") ?? ""], sorting: chosenSorting, + searchPhrase: searchText, }); const loadNextPageRef = useRef(null); @@ -150,7 +153,7 @@ export const DashboardGovernanceActionsCategory = () => { sortOpen={sortOpen} /> - {isProposalsLoading || isDrepLoading ? ( + {!mappedData || isEnableLoading || isProposalsLoading ? ( @@ -172,13 +175,12 @@ export const DashboardGovernanceActionsCategory = () => { {mappedData.map((item) => ( @@ -186,33 +188,34 @@ export const DashboardGovernanceActionsCategory = () => { {...item} index={item.index} inProgress={ - voteTransaction.proposalId === item.txHash + item.index + pendingTransaction.vote?.resourceId === + item.txHash + item.index } onClick={() => { saveScrollPosition(); // eslint-disable-next-line no-unused-expressions - voteTransaction.proposalId === item.txHash + item.index + pendingTransaction.vote?.resourceId === item.txHash + item.index ? openInNewTab( - "https://adanordic.com/latest_transactions", - ) + "https://adanordic.com/latest_transactions", + ) : navigate( - generatePath( - PATHS.dashboardGovernanceActionsAction, - { - proposalId: getFullGovActionId( - item.txHash, - item.index, - ), - }, - ), + generatePath( + PATHS.dashboardGovernanceActionsAction, { - state: { - ...item, - openedFromCategoryPage: true, - }, + proposalId: getFullGovActionId( + item.txHash, + item.index, + ), }, - ); + ), + { + state: { + ...item, + openedFromCategoryPage: true, + }, + }, + ); }} txHash={item.txHash} /> diff --git a/govtool/frontend/src/pages/GovernanceActions.tsx b/govtool/frontend/src/pages/GovernanceActions.tsx index 2867a6ccf..08eb11d15 100644 --- a/govtool/frontend/src/pages/GovernanceActions.tsx +++ b/govtool/frontend/src/pages/GovernanceActions.tsx @@ -1,15 +1,23 @@ import { useState, useCallback, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import { Box, Divider } from "@mui/material"; +import { Box, CircularProgress, Divider } from "@mui/material"; import { Background, ScrollToManage, Typography } from "@atoms"; -import { PATHS } from "@consts"; +import { GOVERNANCE_ACTIONS_FILTERS, PATHS } from "@consts"; import { useCardano } from "@context"; -import { useScreenDimension, useTranslation } from "@hooks"; +import { + useGetProposalsQuery, + useScreenDimension, + useTranslation, +} from "@hooks"; import { DataActionsBar } from "@molecules"; import { Footer, TopNav, GovernanceActionsToVote } from "@organisms"; import { WALLET_LS_KEY, getItemFromLocalStorage } from "@utils"; +const defaultCategories = GOVERNANCE_ACTIONS_FILTERS.map( + (category) => category.key +); + export const GovernanceActions = () => { const [searchText, setSearchText] = useState(""); const [filtersOpen, setFiltersOpen] = useState(false); @@ -21,6 +29,15 @@ export const GovernanceActions = () => { const navigate = useNavigate(); const { t } = useTranslation(); + const queryFilters = + chosenFilters.length > 0 ? chosenFilters : defaultCategories; + + const { proposals, isProposalsLoading } = useGetProposalsQuery({ + filters: queryFilters, + sorting: chosenSorting, + searchPhrase: searchText, + }); + useEffect(() => { if (isEnabled && getItemFromLocalStorage(`${WALLET_LS_KEY}_stake_key`)) { navigate(PATHS.dashboardGovernanceActions); @@ -79,12 +96,25 @@ export const GovernanceActions = () => { sortOpen={sortOpen} /> - + {!proposals || isProposalsLoading ? ( + + + + ) : ( + + )}