Skip to content

Commit

Permalink
Merge pull request #458 from IntersectMBO/feat/378-hash-and-validatio…
Browse files Browse the repository at this point in the history
…n-of-the-metadata

[#378] feat: add hash and validation of the metadata
  • Loading branch information
MSzalowski authored Mar 14, 2024
2 parents 45538ca + 854034f commit 318d29d
Show file tree
Hide file tree
Showing 25 changed files with 1,139 additions and 532 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ changes.
- Create GA creation form [Issue 360](https://github.com/IntersectMBO/govtool/issues/360)
- Create TextArea [Issue 110](https://github.com/IntersectMBO/govtool/issues/110)
- Choose GA type - GA Submiter [Issue 358](https://github.com/IntersectMBO/govtool/issues/358)

- Add on-chain inputs validation [Issue 377](https://github.com/IntersectMBO/govtool/issues/377)
- Add hash and validation of the metadata [Issue 378](https://github.com/IntersectMBO/govtool/issues/378)

### Added

Expand Down Expand Up @@ -44,6 +44,7 @@ changes.
- Fixed CSP settings to allow error reports with Sentry [Issue 291](https://github.com/IntersectMBO/govtool/issues/291).

### Changed

- `drep/list` now return also `status` and `type` fields. Also it now returns the retired dreps, and you can search for given drep by name using optional query parameter. If the drep name is passed exactly, then you can even find a drep that's sole voter. [Issue 446](https://github.com/IntersectMBO/govtool/issues/446)
- `drep/list` and `drep/info` endpoints now return additional data such as metadata url and hash, and voting power [Issue 223](https://github.com/IntersectMBO/govtool/issues/223)
- `drep/info` now does not return sole voters (dreps without metadata) [Issue 317](https://github.com/IntersectMBO/govtool/issues/317)
Expand Down
1 change: 1 addition & 0 deletions govtool/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@mui/icons-material": "^5.14.3",
"@mui/material": "^5.14.4",
"@sentry/react": "^7.77.0",
"@types/jsonld": "^1.5.13",
"@types/react": "^18.2.12",
"@types/react-gtm-module": "^2.0.2",
"axios": "^1.4.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { Button, InfoText, Spacer, Typography } from "@atoms";
import { GOVERNANCE_ACTION_FIELDS } from "@consts";
import { useCreateGovernanceActionForm, useTranslation } from "@hooks";
import { Field } from "@molecules";
import { URL_REGEX } from "@/utils";
import { GovernanceActionField } from "@/types/governanceAction";

import { BgCard } from "../BgCard";
import { ControlledField } from "../ControlledField";
import { GovernanceActionField } from "@/types/governanceAction";
import { URL_REGEX } from "@/utils";

const LINK_PLACEHOLDER = "https://website.com/";
const MAX_NUMBER_OF_LINKS = 8;
Expand Down Expand Up @@ -117,10 +117,6 @@ export const CreateGovernanceActionForm = ({
placeholder={LINK_PLACEHOLDER}
name={`links.${index}.link`}
rules={{
required: {
value: true,
message: t("createGovernanceAction.fields.validations.required"),
},
pattern: {
value: URL_REGEX,
message: t("createGovernanceAction.fields.validations.url"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@ export const ReviewCreatedGovernanceAction = ({
key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " ");

return (
<Box sx={{ mb: 5 }}>
<Box sx={{ mb: 5, width: "100%" }}>
<Typography color="neutralGray" fontWeight={400} variant="body2">
{label}
</Typography>
<Typography sx={{ mt: 0.5 }} variant="body2">
<Typography
sx={{ mt: 0.5, overflow: "hidden", textOverflow: "ellipsis" }}
variant="body2"
>
{value as string}
</Typography>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import { Dispatch, SetStateAction, useCallback } from "react";
import { Box } from "@mui/material";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";

Expand All @@ -7,7 +7,7 @@ import { ICONS } from "@consts";
import { useCreateGovernanceActionForm, useTranslation } from "@hooks";
import { Step } from "@molecules";
import { BgCard, ControlledField } from "@organisms";
import { URL_REGEX, downloadJson, openInNewTab } from "@utils";
import { URL_REGEX, openInNewTab } from "@utils";

type StorageInformationProps = {
setStep: Dispatch<SetStateAction<number>>;
Expand All @@ -19,11 +19,11 @@ export const StorageInformation = ({ setStep }: StorageInformationProps) => {
control,
errors,
createGovernanceAction,
generateJsonBody,
getValues,
watch,
} = useCreateGovernanceActionForm();
const [isJsonDownloaded, setIsJsonDownloaded] = useState<boolean>(false);
onClickDownloadJson,
isLoading,
} = useCreateGovernanceActionForm(setStep);

// TODO: change on correct file name
const fileName = getValues("governance_action_type");
Expand All @@ -34,24 +34,18 @@ export const StorageInformation = ({ setStep }: StorageInformationProps) => {
[]
);

const isActionButtonDisabled = !watch("storingURL") || !isJsonDownloaded;
const isActionButtonDisabled = !watch("storingURL");

const onClickBack = useCallback(() => setStep(5), []);

const onClickDownloadJson = async () => {
const data = getValues();
const jsonBody = await generateJsonBody(data);
downloadJson(jsonBody, fileName);
setIsJsonDownloaded(true);
};

return (
<BgCard
actionButtonLabel={t("continue")}
backButtonLabel={t("back")}
isActionButtonDisabled={isActionButtonDisabled}
onClickActionButton={createGovernanceAction}
onClickBackButton={onClickBack}
isLoadingActionButton={isLoading}
>
<Typography sx={{ textAlign: "center" }} variant="headline4">
{t("createGovernanceAction.storingInformationTitle")}
Expand Down
23 changes: 15 additions & 8 deletions govtool/frontend/src/components/organisms/DashboardCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ export const DashboardCards = () => {
buildSignSubmitConwayCertTx,
delegateTo,
delegateTransaction,
voter,
dRepID,
dRepIDBech32,
govActionTransaction,
isDrepLoading,
isPendingTransaction,
registerTransaction,
soleVoterTransaction,
stakeKey,
voter,
} = useCardano();
const navigate = useNavigate();
const { currentDelegation, isCurrentDelegationLoading } =
Expand Down Expand Up @@ -185,6 +186,15 @@ export const DashboardCards = () => {
[isPendingTransaction, navigate]
);

const onClickGovernanceActionCardActionButton = useCallback(() => {
if(govActionTransaction.transactionHash) {
navigate(PATHS.dashboardGovernanceActions)
return
}
navigate(PATHS.createGovernanceAction)

}, [govActionTransaction.transactionHash, navigate])

const displayedDelegationId = useMemo(() => {
const restrictedNames = [
dRepID,
Expand Down Expand Up @@ -525,16 +535,13 @@ export const DashboardCards = () => {
<DashboardActionCard
dataTestidFirstButton="propose-governance-actions-button"
description={t("dashboard.proposeGovernanceAction.description")}
// TODO: add isPendingGovernanceAction to the context
// inProgress={isPendingGovernanceAction}
firstButtonAction={() => navigate(PATHS.createGovernanceAction)}
firstButtonAction={onClickGovernanceActionCardActionButton}
firstButtonLabel={t(
`dashboard.proposeGovernanceAction.${
// TODO: add isPendingGovernanceAction to the context
// isPendingGovernanceAction ? "propose" : "viewGovernanceActions"
`propose`
govActionTransaction.transactionHash ? "view" : "propose"
}`
)}
)}
inProgress={!!govActionTransaction.transactionHash}
secondButtonLabel={t("learnMore")}
secondButtonAction={() =>
openInNewTab(
Expand Down
46 changes: 43 additions & 3 deletions govtool/frontend/src/components/organisms/StatusModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import { useScreenDimension, useTranslation } from "@/hooks";

export interface StatusModalState {
buttonText?: string;
cancelText?: string;
feedbackText?: string;
status: "warning" | "info" | "success";
isInfo?: boolean;
link?: string;
linkText?: string;
message: React.ReactNode;
onSubmit?: () => void;
onCancel?: () => void;
onFeedback?: () => void;
title: string;
dataTestId: string;
}
Expand Down Expand Up @@ -43,32 +48,67 @@ export function StatusModal() {
textAlign="center"
sx={{ fontSize: "16px", fontWeight: "400" }}
>
{state?.message}{" "}
{state?.message}
{state?.link && (
<Link
onClick={() => openInNewTab(state?.link || "")}
target="_blank"
sx={[{ "&:hover": { cursor: "pointer" } }]}
>
{t("thisLink")}
{state?.linkText || t("thisLink")}
</Link>
)}
</Typography>
</ModalContents>
<Button
data-testid={"confirm-modal-button"}
data-testid="confirm-modal-button"
onClick={state?.onSubmit ? state?.onSubmit : closeModal}
sx={{
borderRadius: 50,
margin: "0 auto",
padding: "10px 26px",
textTransform: "none",
marginTop: "38px",
width: "100%",
}}
variant="contained"
>
{state?.buttonText || t("confirm")}
</Button>
{state?.cancelText && (
<Button
data-testid="cancel-modal-button"
onClick={state?.onCancel ? state?.onCancel : closeModal}
sx={{
borderRadius: 50,
margin: "0 auto",
padding: "10px 26px",
textTransform: "none",
marginTop: "24px",
width: "100%",
}}
variant="outlined"
>
{state?.cancelText}
</Button>
)}
{state?.feedbackText && (
<Button
data-testid="feedback-button"
onClick={state?.onFeedback ? state?.onFeedback : closeModal}
sx={{
borderRadius: 50,
margin: "0 auto",
padding: "10px 26px",
textTransform: "none",
marginTop: "24px",
width: "100%",
}}
variant="outlined"
>
{state?.feedbackText}
</Button>
)}
</ModalWrapper>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,15 @@ export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = {
placeholderI18nKey:
"createGovernanceAction.fields.declarations.receivingAddress.placeholder",
rules: {
validate: (value) => {
if (bech32.decode(value).words.length) {
return true;
} else {
validate: async (value) => {
try {
const decoded = await bech32.decode(value);
if (decoded.words.length) {
return true;
} else {
throw new Error();
}
} catch (error) {
return I18n.t("createGovernanceAction.fields.validations.bech32");
}
},
Expand All @@ -114,19 +119,15 @@ export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = {
value: true,
message: I18n.t("createGovernanceAction.fields.validations.required"),
},
validate: (value) => {
if (Number.isInteger(Number(value))) {
return true;
} else {
return I18n.t("createGovernanceAction.fields.validations.number");
}
},
validate: (value) =>
Number.isInteger(Number(value)) ||
I18n.t("createGovernanceAction.fields.validations.number"),
},
},
},
} as const;

const commonContext = {
export const GOVERNANCE_ACTION_CONTEXT = {
"@language": "en-us",
CIP100:
"https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#",
Expand Down Expand Up @@ -175,18 +176,3 @@ const commonContext = {
},
},
};

export const GOVERNANCE_ACTION_CONTEXTS = {
[GovernanceActionType.Info]: commonContext,
[GovernanceActionType.Treasury]: {
...commonContext,
body: {
...commonContext.body,
"@context": {
...commonContext.body["@context"],
amount: "CIP108:amount",
receivingAddress: "CIP108:receivingAddress",
},
},
},
};
5 changes: 5 additions & 0 deletions govtool/frontend/src/consts/governanceAction/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./fields";
export * from "./filters";
export * from "./sorting";
export * from "./modalConfig";
export * from "./metadataHashValidationErrors";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum MetadataHashValidationErrors {
INVALID_URL = "Invalid URL",
INVALID_JSON = "Invalid JSON",
INVALID_HASH = "Invalid hash",
FETCH_ERROR = "Error fetching data",
}
50 changes: 50 additions & 0 deletions govtool/frontend/src/consts/governanceAction/modalConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ModalState } from "@/context";
import I18n from "@/i18n";

import { MetadataHashValidationErrors } from "./metadataHashValidationErrors";

const externalDataDoesntMatchModal = {
status: "warning",
title: I18n.t("createGovernanceAction.modals.externalDataDoesntMatch.title"),
message: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.message"
),
buttonText: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.buttonText"
),
cancelText: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.cancelRegistrationText"
),
feedbackText: I18n.t(
"createGovernanceAction.modals.externalDataDoesntMatch.feedbackText"
),
} as const;

const urlCannotBeFound = {
status: "warning",
title: I18n.t("createGovernanceAction.modals.urlCannotBeFound.title"),
message: I18n.t("createGovernanceAction.modals.urlCannotBeFound.message"),
link: "https://docs.sanchogov.tools",
linkText: I18n.t("createGovernanceAction.modals.urlCannotBeFound.linkText"),
buttonText: I18n.t(
"createGovernanceAction.modals.urlCannotBeFound.buttonText"
),
cancelText: I18n.t(
"createGovernanceAction.modals.urlCannotBeFound.cancelRegistrationText"
),
feedbackText: I18n.t(
"createGovernanceAction.modals.urlCannotBeFound.feedbackText"
),
};

export const storageInformationErrorModals: Record<
MetadataHashValidationErrors,
ModalState<
typeof externalDataDoesntMatchModal | typeof urlCannotBeFound
>["state"]
> = {
[MetadataHashValidationErrors.INVALID_URL]: urlCannotBeFound,
[MetadataHashValidationErrors.FETCH_ERROR]: urlCannotBeFound,
[MetadataHashValidationErrors.INVALID_JSON]: externalDataDoesntMatchModal,
[MetadataHashValidationErrors.INVALID_HASH]: externalDataDoesntMatchModal,
};
Loading

0 comments on commit 318d29d

Please sign in to comment.