diff --git a/CHANGELOG.md b/CHANGELOG.md index 6faf8440c..44ec42e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ changes. - Fix displaying modals to not block signing transactions [Issue 710](https://github.com/IntersectMBO/govtool/issues/710) - Change style of url button to trim the file name [Issue 655](https://github.com/IntersectMBO/govtool/issues/655) - Change regex for parsing urls to match urls without protocol [Issue 655](https://github.com/IntersectMBO/govtool/issues/655) +- Integrate ga displaying metadata validation with the validation service [Issue 712](https://github.com/IntersectMBO/govtool/issues/712) ### Added diff --git a/govtool/frontend/src/components/molecules/Breadcrumbs.tsx b/govtool/frontend/src/components/molecules/Breadcrumbs.tsx index c0766d345..83f191034 100644 --- a/govtool/frontend/src/components/molecules/Breadcrumbs.tsx +++ b/govtool/frontend/src/components/molecules/Breadcrumbs.tsx @@ -4,12 +4,14 @@ import Divider from "@mui/material/Divider"; import { useScreenDimension } from "@hooks"; import { Typography } from "@atoms"; +import { getMetadataDataMissingStatusTranslation } from "@/utils"; +import { MetadataValidationStatus } from "@/models"; type BreadcrumbsProps = { elementOne: string; elementOnePath: To; elementTwo: string; - isDataMissing: boolean; + isDataMissing: MetadataValidationStatus | boolean; }; export const Breadcrumbs = ({ @@ -19,7 +21,6 @@ export const Breadcrumbs = ({ isDataMissing, }: BreadcrumbsProps) => { const { isMobile } = useScreenDimension(); - return ( - {isDataMissing || elementTwo} + {(isDataMissing !== false && + getMetadataDataMissingStatusTranslation( + isDataMissing as MetadataValidationStatus, + )) || + elementTwo} ); diff --git a/govtool/frontend/src/components/molecules/DataMissingInfoBox.tsx b/govtool/frontend/src/components/molecules/DataMissingInfoBox.tsx index f13da7ec2..1770a6806 100644 --- a/govtool/frontend/src/components/molecules/DataMissingInfoBox.tsx +++ b/govtool/frontend/src/components/molecules/DataMissingInfoBox.tsx @@ -2,40 +2,43 @@ import { Box, Link } from "@mui/material"; import { Typography } from "@atoms"; import { useTranslation } from "@hooks"; -import { GAMetedataErrors, openInNewTab } from "@utils"; +import { openInNewTab } from "@utils"; +import { MetadataValidationStatus } from "@/models"; export const DataMissingInfoBox = ({ isDataMissing, isInProgress, isSubmitted, }: { - isDataMissing: boolean | GAMetedataErrors; + isDataMissing: boolean | MetadataValidationStatus; isInProgress?: boolean; isSubmitted?: boolean; }) => { const { t } = useTranslation(); const gaMetadataErrorMessage = { - [GAMetedataErrors.DATA_MISSING]: t("errors.gAMetadata.message.dataMissing"), - [GAMetedataErrors.INCORRECT_FORMAT]: t( + [MetadataValidationStatus.URL_NOT_FOUND]: t( + "errors.gAMetadata.message.dataMissing", + ), + [MetadataValidationStatus.INVALID_JSONLD]: t( "errors.gAMetadata.message.incorrectFormat", ), - [GAMetedataErrors.NOT_VERIFIABLE]: t( + [MetadataValidationStatus.INVALID_HASH]: t( "errors.gAMetadata.message.notVerifiable", ), - }[isDataMissing as GAMetedataErrors]; + }[isDataMissing as MetadataValidationStatus]; const gaMetadataErrorDescription = { - [GAMetedataErrors.DATA_MISSING]: t( + [MetadataValidationStatus.URL_NOT_FOUND]: t( "errors.gAMetadata.description.dataMissing", ), - [GAMetedataErrors.INCORRECT_FORMAT]: t( + [MetadataValidationStatus.INVALID_JSONLD]: t( "errors.gAMetadata.description.incorrectFormat", ), - [GAMetedataErrors.NOT_VERIFIABLE]: t( + [MetadataValidationStatus.INVALID_HASH]: t( "errors.gAMetadata.description.notVerifiable", ), - }[isDataMissing as GAMetedataErrors]; + }[isDataMissing as MetadataValidationStatus]; return isDataMissing && !isSubmitted && !isInProgress ? ( @@ -38,11 +39,17 @@ export const GovernanceActionCardHeader = ({ ...(isDataMissing && { color: "#9E2323" }), }} > - {isDataMissing || title} + {(isDataMissing !== false && + getMetadataDataMissingStatusTranslation( + isDataMissing as MetadataValidationStatus, + )) || + title} {isDataMissing && typeof isDataMissing === "string" && ( - {isDataMissing || title} + {(isDataMissing !== false && + getMetadataDataMissingStatusTranslation( + isDataMissing as MetadataValidationStatus, + )) || + title} diff --git a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCard.tsx b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCard.tsx index 689c5b24a..d66a05a1c 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCard.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCard.tsx @@ -7,7 +7,7 @@ import { GovernanceActionDetailsCardVotes, } from "@molecules"; import { GovernanceActionDetailsCardData } from "@organisms"; -import { GAMetedataErrors } from "@utils"; +import { MetadataValidationStatus } from "@models"; type GovernanceActionDetailsCardProps = { abstainVotes: number; @@ -25,7 +25,7 @@ type GovernanceActionDetailsCardProps = { rationale?: string; yesVotes: number; govActionId: string; - isDataMissing: boolean | GAMetedataErrors; + isDataMissing: boolean | MetadataValidationStatus; isDashboard?: boolean; isVoter?: boolean; voteFromEP?: string; diff --git a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx index 617979d9a..da838262e 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx @@ -10,7 +10,8 @@ import { GovernanceActionDetailsCardOnChainData, } from "@molecules"; import { useScreenDimension, useTranslation } from "@hooks"; -import { GAMetedataErrors, getProposalTypeNoEmptySpaces } from "@utils"; +import { getProposalTypeNoEmptySpaces } from "@utils"; +import { MetadataValidationStatus } from "@models"; type GovernanceActionDetailsCardDataProps = { type: string; @@ -25,7 +26,7 @@ type GovernanceActionDetailsCardDataProps = { about?: string; motivation?: string; rationale?: string; - isDataMissing: boolean | GAMetedataErrors; + isDataMissing: boolean | MetadataValidationStatus; isOneColumn: boolean; isDashboard?: boolean; isInProgress?: boolean; diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index 78ef05249..714251b82 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -714,6 +714,11 @@ export const en = { usingUnregisteredStakeKeys: "Warning, no registered stake keys, using unregistered stake keys", }, + dataMissingErrors: { + dataMissing: "Data Missing", + notVerifiable: "Not Verifiable", + incorrectFormat: "Incorrect Format", + }, about: "About", abstain: "Abstain", addLink: "+ Add link", diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index 0686601b7..6c61c77c3 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -1,4 +1,4 @@ -import { GAMetedataErrors } from "@utils"; +import { MetadataValidationStatus } from "@models"; export interface VoterInfo { isRegisteredAsDRep: boolean; @@ -22,7 +22,7 @@ export interface DRepData { deposit: number; votingPower: number; status: DRepStatus; - type: 'DRep' | 'SoleVoter'; + type: "DRep" | "SoleVoter"; } export type Vote = "yes" | "no" | "abstain"; @@ -70,5 +70,7 @@ export interface VotedProposal { } export type VotedProposalToDisplay = { vote: ProposalVote; - proposal: ProposalData & { isDataMissing: boolean | GAMetedataErrors }; + proposal: ProposalData & { + isDataMissing: boolean | MetadataValidationStatus; + }; }; diff --git a/govtool/frontend/src/models/metadataValidation.ts b/govtool/frontend/src/models/metadataValidation.ts index 284a8821c..e36e7c226 100644 --- a/govtool/frontend/src/models/metadataValidation.ts +++ b/govtool/frontend/src/models/metadataValidation.ts @@ -10,7 +10,12 @@ export type ValidateMetadataResult = { valid: boolean; }; +export enum MetadataStandard { + CIP108 = "CIP108", +} + export type MetadataValidationDTO = { url: string; hash: string; + standard?: MetadataStandard; }; diff --git a/govtool/frontend/src/stories/GovernanceAction.stories.ts b/govtool/frontend/src/stories/GovernanceAction.stories.ts index fece9f0d6..76f2408f9 100644 --- a/govtool/frontend/src/stories/GovernanceAction.stories.ts +++ b/govtool/frontend/src/stories/GovernanceAction.stories.ts @@ -1,7 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react"; import { within, userEvent, waitFor, screen } from "@storybook/testing-library"; import { expect, jest } from "@storybook/jest"; -import { GAMetedataErrors, formatDisplayDate } from "@utils"; +import { formatDisplayDate } from "@utils"; +import { MetadataValidationStatus } from "@models"; import { GovernanceActionCard } from "@/components/molecules"; const meta = { @@ -74,20 +75,20 @@ export const GovernanceActionCardIsLoading: Story = { export const GovernanceActionCardDataMissing: Story = { args: { ...commonArgs, - isDataMissing: GAMetedataErrors.DATA_MISSING, + isDataMissing: MetadataValidationStatus.URL_NOT_FOUND, }, }; export const GovernanceActionCardIncorectFormat: Story = { args: { ...commonArgs, - isDataMissing: GAMetedataErrors.INCORRECT_FORMAT, + isDataMissing: MetadataValidationStatus.INVALID_JSONLD, }, }; export const GovernanceActionCardNotVerifiable: Story = { args: { ...commonArgs, - isDataMissing: GAMetedataErrors.NOT_VERIFIABLE, + isDataMissing: MetadataValidationStatus.INVALID_HASH, }, }; diff --git a/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts b/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts index cb9700c35..bd9a67c4f 100644 --- a/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts +++ b/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { screen, userEvent, waitFor, within } from "@storybook/testing-library"; import { GovernanceActionDetailsCard } from "@organisms"; import { expect } from "@storybook/jest"; -import { GAMetedataErrors } from "@/utils"; +import { MetadataValidationStatus } from "@models"; const meta = { title: "Example/GovernanceActionDetailsCard", @@ -79,20 +79,20 @@ export const GovernanceActionDetailsDrep: Story = { export const GovernanceActionDetailsDataMissing: Story = { args: { ...commonArgs, - isDataMissing: GAMetedataErrors.DATA_MISSING, + isDataMissing: MetadataValidationStatus.URL_NOT_FOUND, }, }; export const GovernanceActionDetailsIncorrectFormat: Story = { args: { ...commonArgs, - isDataMissing: GAMetedataErrors.INCORRECT_FORMAT, + isDataMissing: MetadataValidationStatus.INVALID_JSONLD, }, }; export const GovernanceActionDetailsNotVerifiable: Story = { args: { ...commonArgs, - isDataMissing: GAMetedataErrors.NOT_VERIFIABLE, + isDataMissing: MetadataValidationStatus.INVALID_HASH, }, }; diff --git a/govtool/frontend/src/types/global.d.ts b/govtool/frontend/src/types/global.d.ts index 0640e6c20..6204ffc90 100644 --- a/govtool/frontend/src/types/global.d.ts +++ b/govtool/frontend/src/types/global.d.ts @@ -1,4 +1,4 @@ -import { GAMetedataErrors } from "@utils"; +import { MetadataValidationStatus } from "@models"; export {}; @@ -40,7 +40,7 @@ declare global { }; type ActionTypeToDsiplay = ActionType & { - isDataMissing: boolean | GAMetedataErrors; + isDataMissing: boolean | MetadataValidationStatus; }; interface ActionVotedOnType extends ActionTypeToDsiplay { diff --git a/govtool/frontend/src/utils/getMetadataDataMissingStatusTranslation.ts b/govtool/frontend/src/utils/getMetadataDataMissingStatusTranslation.ts new file mode 100644 index 000000000..0a91531a6 --- /dev/null +++ b/govtool/frontend/src/utils/getMetadataDataMissingStatusTranslation.ts @@ -0,0 +1,19 @@ +import i18n from "@/i18n"; +import { MetadataValidationStatus } from "@/models"; + +/** + * Retrieves the translation for the given metadata validation status. + * + * @param status - The metadata validation status. + * @returns The translated string corresponding to the status. + */ +export const getMetadataDataMissingStatusTranslation = ( + status: MetadataValidationStatus, +): string => { + const errorKey = { + [MetadataValidationStatus.URL_NOT_FOUND]: "dataMissing", + [MetadataValidationStatus.INVALID_JSONLD]: "incorrectFormat", + [MetadataValidationStatus.INVALID_HASH]: "notVerifiable", + }[status] as "dataMissing" | "incorrectFormat" | "notVerifiable"; + return i18n.t(`dataMissingErrors.${errorKey || "dataMissing"}`); +}; diff --git a/govtool/frontend/src/utils/index.ts b/govtool/frontend/src/utils/index.ts index e1069f5c2..2a0eb135e 100644 --- a/govtool/frontend/src/utils/index.ts +++ b/govtool/frontend/src/utils/index.ts @@ -14,6 +14,7 @@ export * from "./generateJsonld"; export * from "./getDRepID"; export * from "./getGovActionId"; export * from "./getLengthInBytes"; +export * from "./getMetadataDataMissingStatusTranslation"; export * from "./getProposalTypeLabel"; export * from "./isValidFormat"; export * from "./jsonUtils"; diff --git a/govtool/frontend/src/utils/tests/getMetadataDataMissingStatusTranslation.test.ts b/govtool/frontend/src/utils/tests/getMetadataDataMissingStatusTranslation.test.ts new file mode 100644 index 000000000..230dedf48 --- /dev/null +++ b/govtool/frontend/src/utils/tests/getMetadataDataMissingStatusTranslation.test.ts @@ -0,0 +1,32 @@ +import { MetadataValidationStatus } from "@models"; +import { getMetadataDataMissingStatusTranslation } from "../getMetadataDataMissingStatusTranslation"; + +describe("getMetadataDataMissingStatusTranslation", () => { + it("should return the correct translation for URL_NOT_FOUND status", () => { + const translation = getMetadataDataMissingStatusTranslation( + MetadataValidationStatus.URL_NOT_FOUND, + ); + expect(translation).toBe("Data Missing"); + }); + + it("should return the correct translation for INVALID_JSONLD status", () => { + const translation = getMetadataDataMissingStatusTranslation( + MetadataValidationStatus.INVALID_JSONLD, + ); + expect(translation).toBe("Incorrect Format"); + }); + + it("should return the correct translation for INVALID_HASH status", () => { + const translation = getMetadataDataMissingStatusTranslation( + MetadataValidationStatus.INVALID_HASH, + ); + expect(translation).toBe("Not Verifiable"); + }); + + it("should return the default translation for unknown status", () => { + const translation = getMetadataDataMissingStatusTranslation( + "UNKNOWN_STATUS" as MetadataValidationStatus, + ); + expect(translation).toBe("Data Missing"); + }); +}); diff --git a/govtool/frontend/src/utils/validateMetadataHash.ts b/govtool/frontend/src/utils/validateMetadataHash.ts index af8ece41d..d9eb0e1e8 100644 --- a/govtool/frontend/src/utils/validateMetadataHash.ts +++ b/govtool/frontend/src/utils/validateMetadataHash.ts @@ -1,15 +1,6 @@ -import * as blake from "blakejs"; +import { postValidate } from "@services"; -import { API } from "@services"; -import { sharedGovernanceActionFields } from "@consts"; - -import { URL_REGEX, areObjectsTheSame, canonizeJSON } from "."; - -export enum GAMetedataErrors { - DATA_MISSING = "Data Missing", - NOT_VERIFIABLE = "Data Not Verifiable", - INCORRECT_FORMAT = "Data Formatted Incorrectly", -} +import { MetadataStandard, MetadataValidationStatus } from "@/models"; export const checkIsMissingGAMetadata = async ({ url, @@ -17,45 +8,18 @@ export const checkIsMissingGAMetadata = async ({ }: { url: string; hash: string; -}): Promise => { - if (!url?.match(URL_REGEX)) { - return GAMetedataErrors.DATA_MISSING; - } - - let gaMetadata; - try { - const { data } = await API.get(url); - gaMetadata = data; - } catch (e) { - return GAMetedataErrors.DATA_MISSING; - } - const JSONBody = gaMetadata?.body; - - if (!JSONBody) { - return GAMetedataErrors.DATA_MISSING; - } - - const govtoolFields = { - ...sharedGovernanceActionFields, - references: [], - }; - - if (!areObjectsTheSame(JSONBody, govtoolFields)) { - return GAMetedataErrors.INCORRECT_FORMAT; - } - - let canonizedGAMetadata; +}): Promise => { try { - canonizedGAMetadata = await canonizeJSON(gaMetadata); + const { status } = await postValidate({ + url, + hash, + standard: MetadataStandard.CIP108, + }); + if (status) { + return status; + } + return false; } catch (error) { - return GAMetedataErrors.INCORRECT_FORMAT; + return MetadataValidationStatus.URL_NOT_FOUND; } - - const gaHash = blake.blake2bHex(canonizedGAMetadata, undefined, 32); - - if (gaHash !== hash) { - return GAMetedataErrors.NOT_VERIFIABLE; - } - - return false; };