diff --git a/.github/workflows/release-gear-idea.yml b/.github/workflows/release-gear-idea.yml index 7f46448802..3d857d2c6b 100644 --- a/.github/workflows/release-gear-idea.yml +++ b/.github/workflows/release-gear-idea.yml @@ -104,6 +104,7 @@ jobs: VITE_INDEXER_API_URL=${{ secrets.VITE_INDEXER_API_URL }} VITE_TESTNET_DNS_API_URL=${{ secrets.VITE_TESTNET_DNS_API_URL }} VITE_MAINNET_DNS_API_URL=${{ secrets.VITE_MAINNET_DNS_API_URL }} + VITE_CODE_VERIFIER_API_URL=${{ secrets.VITE_CODE_VERIFIER_API_URL }} build-faucet-image: runs-on: ubuntu-latest @@ -210,14 +211,7 @@ jobs: tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-gear-idea-explorer:${{ env.ENVIRONMENT }} deploy-to-k8s: - needs: - [ - build-frontend-image, - build-faucet-image, - build-meta-storage, - build-squid-image, - build-squid-explorer - ] + needs: [build-frontend-image, build-faucet-image, build-meta-storage, build-squid-image, build-squid-explorer] runs-on: ubuntu-latest steps: @@ -239,7 +233,6 @@ jobs: echo "deployments=squid-testnet-v2 explorer frontend-nginx meta-storage faucet" fi - - name: Deploy to k8s uses: sergeyfilyanin/kubectl-aws-eks@master with: diff --git a/idea/gear/common/package.json b/idea/gear/common/package.json index 2e139653fb..d49be5f0c9 100644 --- a/idea/gear/common/package.json +++ b/idea/gear/common/package.json @@ -1,7 +1,7 @@ { "name": "gear-idea-common", "private": true, - "version": "1.0.0", + "version": "1.1.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/idea/gear/explorer/package.json b/idea/gear/explorer/package.json index 0d43f8c212..c9fc9e3941 100644 --- a/idea/gear/explorer/package.json +++ b/idea/gear/explorer/package.json @@ -1,14 +1,14 @@ { "name": "gear-idea-explorer", - "version": "1.0.0", + "version": "1.1.0", "private": true, "dependencies": { "class-validator": "0.14.1", "cron": "^3.1.7", "dotenv": "^16.4.5", "express": "4.19.2", - "gear-idea-common": "1.0.0", - "gear-idea-indexer-db": "1.0.0", + "gear-idea-common": "1.1.0", + "gear-idea-indexer-db": "1.1.0", "nanoid": "^5.0.7", "pg": "8.12.0", "redis": "^4.6.15", diff --git a/idea/gear/faucet/package.json b/idea/gear/faucet/package.json index 1cd055a4d5..022f568dbd 100644 --- a/idea/gear/faucet/package.json +++ b/idea/gear/faucet/package.json @@ -1,7 +1,7 @@ { "name": "gear-idea-faucet", "private": true, - "version": "1.0.0", + "version": "1.1.0", "main": "server.js", "scripts": { "build": "rm -rf dist && npx tsc", @@ -20,7 +20,7 @@ "cron": "^3.1.6", "dotenv": "10.0.0", "express": "4.18.1", - "gear-idea-common": "1.0.0", + "gear-idea-common": "1.1.0", "hcaptcha": "0.1.1", "nodemon": "2.0.16", "pg": "8.7.1", diff --git a/idea/gear/frontend/.env.example b/idea/gear/frontend/.env.example index 9e09aa27ce..6ce419dffa 100644 --- a/idea/gear/frontend/.env.example +++ b/idea/gear/frontend/.env.example @@ -8,3 +8,4 @@ VITE_GTM_ID= VITE_DEFAULT_TRANSFER_BALANCE_VALUE= VITE_MAINNET_DNS_API_URL= VITE_TESTNET_DNS_API_URL= +VITE_CODE_VERIFIER_API_URL= diff --git a/idea/gear/frontend/Dockerfile b/idea/gear/frontend/Dockerfile index 19d04d2abc..52bada34bc 100644 --- a/idea/gear/frontend/Dockerfile +++ b/idea/gear/frontend/Dockerfile @@ -17,7 +17,8 @@ ARG VITE_NODE_ADDRESS \ VITE_TESTNET_VOUCHERS_API_URL \ VITE_INDEXER_API_URL \ VITE_MAINNET_DNS_API_URL \ - VITE_TESTNET_DNS_API_URL + VITE_TESTNET_DNS_API_URL \ + VITE_CODE_VERIFIER_API_URL ENV VITE_NODE_ADDRESS=${VITE_NODE_ADDRESS} \ VITE_NODES_API_URL=${VITE_NODES_API_URL} \ @@ -30,7 +31,8 @@ ENV VITE_NODE_ADDRESS=${VITE_NODE_ADDRESS} \ VITE_TESTNET_VOUCHERS_API_URL=${VITE_TESTNET_VOUCHERS_API_URL} \ VITE_INDEXER_API_URL=${VITE_INDEXER_API_URL} \ VITE_MAINNET_DNS_API_URL=${VITE_MAINNET_DNS_API_URL} \ - VITE_TESTNET_DNS_API_URL=${VITE_TESTNET_DNS_API_URL} + VITE_TESTNET_DNS_API_URL=${VITE_TESTNET_DNS_API_URL} \ + VITE_CODE_VERIFIER_API_URL=${VITE_CODE_VERIFIER_API_URL} RUN yarn build:gear-idea-frontend diff --git a/idea/gear/frontend/package.json b/idea/gear/frontend/package.json index f84afce8ac..45dc8ba8d3 100644 --- a/idea/gear/frontend/package.json +++ b/idea/gear/frontend/package.json @@ -1,6 +1,6 @@ { "name": "gear-idea-frontend", - "version": "1.0.0", + "version": "1.1.0", "private": true, "scripts": { "start": "npx vite --open --port 3000", @@ -38,7 +38,7 @@ "react-gtm-module": "2.0.11", "react-hook-form": "7.52.2", "react-number-format": "5.3.1", - "react-router-dom": "6.16.0", + "react-router-dom": "6.28.2", "react-transition-group": "4.4.5", "sails-js": "0.3.0", "sails-js-parser": "0.1.0", diff --git a/idea/gear/frontend/src/features/code-verifier/api/consts.ts b/idea/gear/frontend/src/features/code-verifier/api/consts.ts new file mode 100644 index 0000000000..aed4717330 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/api/consts.ts @@ -0,0 +1,9 @@ +const API_URL = import.meta.env.VITE_CODE_VERIFIER_API_URL as string; + +const METHOD = { + VERIFY: 'verify', + VERIFY_STATUS: 'verify/status', + CODE: 'code', +} as const; + +export { API_URL, METHOD }; diff --git a/idea/gear/frontend/src/features/code-verifier/api/hooks.ts b/idea/gear/frontend/src/features/code-verifier/api/hooks.ts new file mode 100644 index 0000000000..a94d9e9dd3 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/api/hooks.ts @@ -0,0 +1,54 @@ +import { HexString } from '@gear-js/api'; +import { useAlert } from '@gear-js/react-hooks'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { STATUS_CODES } from 'http'; +import { useEffect } from 'react'; + +import { getVerificationStatus, getVerifiedCode, verifyCode } from './requests'; + +function useVerifyCode() { + return useMutation({ + mutationKey: ['verify-code'], + mutationFn: verifyCode, + }); +} + +function useIsCodeVerified(codeId: HexString | null | undefined) { + const alert = useAlert(); + + const query = useQuery({ + queryKey: ['code-verification-status', codeId], + queryFn: () => getVerifiedCode(codeId!), + select: (response) => Boolean(response), + enabled: Boolean(codeId), + }); + + const { error } = query; + + useEffect(() => { + if (error && error.message !== STATUS_CODES[404]) alert.error(error.message); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error]); + + return query; +} + +function useVerificationStatus(id: string) { + const alert = useAlert(); + + const query = useQuery({ + queryKey: ['verification-status', id], + queryFn: () => getVerificationStatus(id), + }); + + const { error } = query; + + useEffect(() => { + if (error) alert.error(error.message); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error]); + + return query; +} + +export { useVerifyCode, useIsCodeVerified, useVerificationStatus }; diff --git a/idea/gear/frontend/src/features/code-verifier/api/index.ts b/idea/gear/frontend/src/features/code-verifier/api/index.ts new file mode 100644 index 0000000000..7943b29456 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/api/index.ts @@ -0,0 +1,3 @@ +import { useVerifyCode, useIsCodeVerified, useVerificationStatus } from './hooks'; + +export { useVerifyCode, useIsCodeVerified, useVerificationStatus }; diff --git a/idea/gear/frontend/src/features/code-verifier/api/requests.ts b/idea/gear/frontend/src/features/code-verifier/api/requests.ts new file mode 100644 index 0000000000..7e88d3055a --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/api/requests.ts @@ -0,0 +1,13 @@ +import { fetchWithGuard } from '@/shared/helpers'; +import { CodeResponse, StatusResponse, VerifyParameters, VerifyResponse } from './types'; +import { API_URL } from './consts'; + +const verifyCode = (parameters: VerifyParameters) => + fetchWithGuard(`${API_URL}/verify`, 'POST', parameters); + +const getVerificationStatus = (id: string) => + fetchWithGuard(`${API_URL}/verify/status?id=${id}`, 'GET'); + +const getVerifiedCode = (id: string) => fetchWithGuard(`${API_URL}/code?id=${id}`, 'GET'); + +export { verifyCode, getVerificationStatus, getVerifiedCode }; diff --git a/idea/gear/frontend/src/features/code-verifier/api/types.ts b/idea/gear/frontend/src/features/code-verifier/api/types.ts new file mode 100644 index 0000000000..f61e76a5dd --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/api/types.ts @@ -0,0 +1,29 @@ +import { HexString } from '@gear-js/api'; + +type VerifyParameters = { + build_idl: boolean; + code_id: HexString; + network: string; + project: { Name: string } | { PathToCargoToml: string }; + repo_link: string; + version: string; +}; + +type VerifyResponse = { + id: string; +}; + +type StatusResponse = { + status: 'pending' | 'verified' | 'failed' | 'in_progress'; + failed_reason: string | null; + created_at: number; +}; + +type CodeResponse = { + id: string; + idl_hash: string | null; + name: string; + repo_link: string; +}; + +export type { VerifyParameters, VerifyResponse, StatusResponse, CodeResponse }; diff --git a/idea/gear/frontend/src/features/code-verifier/assets/refresh.svg b/idea/gear/frontend/src/features/code-verifier/assets/refresh.svg new file mode 100644 index 0000000000..313661215a --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/assets/refresh.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/idea/gear/frontend/src/features/code-verifier/components/index.ts b/idea/gear/frontend/src/features/code-verifier/components/index.ts new file mode 100644 index 0000000000..b242521ee7 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/components/index.ts @@ -0,0 +1,5 @@ +import { VerifyLink } from './verify-link'; +import { VerificationStatus } from './verification-status'; +import { VerifyForm } from './verify-form'; + +export { VerifyLink, VerificationStatus, VerifyForm }; diff --git a/idea/gear/frontend/src/features/code-verifier/components/verification-status/index.ts b/idea/gear/frontend/src/features/code-verifier/components/verification-status/index.ts new file mode 100644 index 0000000000..3abe5ec2db --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/components/verification-status/index.ts @@ -0,0 +1,3 @@ +import { VerificationStatus } from './verification-status'; + +export { VerificationStatus }; diff --git a/idea/gear/frontend/src/features/code-verifier/components/verification-status/verification-status.module.scss b/idea/gear/frontend/src/features/code-verifier/components/verification-status/verification-status.module.scss new file mode 100644 index 0000000000..9b9ba7a819 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/components/verification-status/verification-status.module.scss @@ -0,0 +1,29 @@ +.status { + padding: 4px 8px; + + font-size: 12px; + font-weight: 600; + line-height: 15.6px; + white-space: nowrap; + + border-radius: 20px; +} + +.verified { + color: #2bd071; + + background-color: rgba(#2bd071, 0.1); +} + +.in_progress, +.pending { + color: #f2c94c; + + background-color: rgba(#f2c94c, 0.1); +} + +.failed { + color: #f24a4a; + + background-color: rgba(#f24a4a, 0.1); +} diff --git a/idea/gear/frontend/src/features/code-verifier/components/verification-status/verification-status.tsx b/idea/gear/frontend/src/features/code-verifier/components/verification-status/verification-status.tsx new file mode 100644 index 0000000000..87e53f754f --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/components/verification-status/verification-status.tsx @@ -0,0 +1,28 @@ +import clsx from 'clsx'; + +import { Skeleton } from '@/shared/ui'; + +import styles from './verification-status.module.scss'; + +type Props = { + value: 'verified' | 'failed' | 'pending' | 'in_progress'; +}; + +const TEXT = { + verified: 'Verified', + failed: 'Failed', + pending: 'Pending', + in_progress: 'In Progress', +} as const; + +function VerificationStatus({ value }: Props) { + return {TEXT[value]}; +} + +function VerificationStatusSkeleton({ disabled }: { disabled: boolean }) { + return ; +} + +VerificationStatus.Skeleton = VerificationStatusSkeleton; + +export { VerificationStatus }; diff --git a/idea/gear/frontend/src/features/code-verifier/components/verify-form/consts.ts b/idea/gear/frontend/src/features/code-verifier/components/verify-form/consts.ts new file mode 100644 index 0000000000..3078728906 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/components/verify-form/consts.ts @@ -0,0 +1,77 @@ +import { z } from 'zod'; + +import { GENESIS } from '@/shared/config'; + +import { isCodeIdValid } from '../../utils'; + +const FIELD_NAME = { + DOCKER_IMAGE_VERSION: 'version', + CODE_ID: 'codeId', + REPO_LINK: 'repoLink', + PROJECT_ID_TYPE: 'projectIdType', + PROJECT_ID: 'projectId', + NETWORK: 'network', + BUILD_IDL: 'buildIdl', +} as const; + +const NETWORK = { + [GENESIS.MAINNET]: 'vara_mainnet', + [GENESIS.TESTNET]: 'vara_testnet', +} as const; + +const NETWORK_OPTIONS = [ + { label: 'Mainnet', value: NETWORK[GENESIS.MAINNET] }, + { label: 'Testnet', value: NETWORK[GENESIS.TESTNET] }, +] as const; + +const PROJECT_ID_TYPE = { + NAME: 'name', + CARGO_TOML_PATH: 'cargoTomlPath', +} as const; + +const DEFAULT_VALUES = { + [FIELD_NAME.DOCKER_IMAGE_VERSION]: '', + [FIELD_NAME.CODE_ID]: '', + [FIELD_NAME.REPO_LINK]: '', + [FIELD_NAME.PROJECT_ID_TYPE]: PROJECT_ID_TYPE.NAME as (typeof PROJECT_ID_TYPE)[keyof typeof PROJECT_ID_TYPE], + [FIELD_NAME.PROJECT_ID]: '', + [FIELD_NAME.NETWORK]: NETWORK_OPTIONS[0].value as (typeof NETWORK)[keyof typeof NETWORK], + [FIELD_NAME.BUILD_IDL]: false, +}; + +const SEMVER_REGEX = /^\d+\.\d+\.\d+$/; +const GITHUB_REPO_URL_REGEX = /^https?:\/\/(www\.)?github\.com\/([\w-]+)\/([\w-]+)(\/.*)?$/; +const CARGO_TOML_PATH_REGEX = /^(?:\.\/)?(?:[^/]+\/)*Cargo\.toml$/; + +const SCHEMA = z + .object({ + [FIELD_NAME.DOCKER_IMAGE_VERSION]: z + .string() + .trim() + .refine((value) => SEMVER_REGEX.test(value), { message: 'Invalid version format' }), + + [FIELD_NAME.CODE_ID]: z + .string() + .trim() + .refine((value) => isCodeIdValid(value), { message: 'Invalid hex' }), + + [FIELD_NAME.REPO_LINK]: z + .string() + .trim() + .refine((value) => GITHUB_REPO_URL_REGEX.test(value), { message: 'Invalid GitHub repository URL' }), + + [FIELD_NAME.PROJECT_ID_TYPE]: z.string(), + [FIELD_NAME.PROJECT_ID]: z.string().trim().min(1), + [FIELD_NAME.NETWORK]: z.string(), + [FIELD_NAME.BUILD_IDL]: z.boolean(), + }) + .refine( + ({ projectIdType, projectId }) => + projectIdType === PROJECT_ID_TYPE.CARGO_TOML_PATH ? CARGO_TOML_PATH_REGEX.test(projectId) : true, + { + message: 'Invalid path to Cargo.toml', + path: [FIELD_NAME.PROJECT_ID], + }, + ); + +export { DEFAULT_VALUES, SCHEMA, NETWORK, FIELD_NAME, PROJECT_ID_TYPE, NETWORK_OPTIONS }; diff --git a/idea/gear/frontend/src/features/code-verifier/components/verify-form/index.ts b/idea/gear/frontend/src/features/code-verifier/components/verify-form/index.ts new file mode 100644 index 0000000000..c2313c02f4 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/components/verify-form/index.ts @@ -0,0 +1,3 @@ +import { VerifyForm } from './verify-form'; + +export { VerifyForm }; diff --git a/idea/gear/frontend/src/features/code-verifier/components/verify-form/verify-form.module.scss b/idea/gear/frontend/src/features/code-verifier/components/verify-form/verify-form.module.scss new file mode 100644 index 0000000000..eb73cecc75 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/components/verify-form/verify-form.module.scss @@ -0,0 +1,24 @@ +.box { + margin-bottom: 24px; + + display: flex; + flex-direction: column; + gap: 48px; +} + +.nestedBox { + display: flex; + flex-direction: column; + gap: 16px; + + background-color: rgba(#fff, 0.03); +} + +.buildIdl { + align-items: center; +} + +.buttons { + display: flex; + gap: 16px; +} diff --git a/idea/gear/frontend/src/features/code-verifier/components/verify-form/verify-form.tsx b/idea/gear/frontend/src/features/code-verifier/components/verify-form/verify-form.tsx new file mode 100644 index 0000000000..655acb96c0 --- /dev/null +++ b/idea/gear/frontend/src/features/code-verifier/components/verify-form/verify-form.tsx @@ -0,0 +1,128 @@ +import { useAlert, useApi } from '@gear-js/react-hooks'; +import { Button, InputWrapper } from '@gear-js/ui'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm } from 'react-hook-form'; +import { generatePath, useNavigate } from 'react-router-dom'; +import { z } from 'zod'; + +import ApplySVG from '@/shared/assets/images/actions/apply.svg?react'; +import { getErrorMessage } from '@/shared/helpers'; +import { BackButton, Box, Input, LabeledCheckbox, Radio, Select } from '@/shared/ui'; + +import { useVerifyCode } from '../../api'; +import { VERIFY_ROUTES } from '../../consts'; +import { useDefaultCodeId } from '../../hooks'; +import { DEFAULT_VALUES, SCHEMA, NETWORK, FIELD_NAME, PROJECT_ID_TYPE, NETWORK_OPTIONS } from './consts'; +import styles from './verify-form.module.scss'; + +type Values = typeof DEFAULT_VALUES; +type FormattedValues = z.infer; + +const INPUT_GAP = '1.5/8.5'; + +function VerifyForm() { + const defaultCodeId = useDefaultCodeId(); + const navigate = useNavigate(); + + const { api, isApiReady } = useApi(); + const genesisHash = isApiReady ? api.genesisHash.toHex() : undefined; + const readOnlyNetwork = defaultCodeId && genesisHash ? NETWORK[genesisHash as keyof typeof NETWORK] : undefined; + + const alert = useAlert(); + + const form = useForm({ + defaultValues: { + ...DEFAULT_VALUES, + [FIELD_NAME.CODE_ID]: defaultCodeId || '', + [FIELD_NAME.NETWORK]: readOnlyNetwork || DEFAULT_VALUES[FIELD_NAME.NETWORK], + }, + + resolver: zodResolver(SCHEMA), + }); + + const projectIdType = form.watch(FIELD_NAME.PROJECT_ID_TYPE); + + const { mutateAsync, isPending } = useVerifyCode(); + + const handleSubmit = ({ version, repoLink, projectId, network, buildIdl, codeId: codeIdValue }: FormattedValues) => { + const project = projectIdType === PROJECT_ID_TYPE.NAME ? { Name: projectId } : { PathToCargoToml: projectId }; + + mutateAsync({ version, network, project, code_id: codeIdValue, repo_link: repoLink, build_idl: buildIdl }) + .then(({ id }) => { + navigate(generatePath(VERIFY_ROUTES.STATUS, { id })); + + alert.success('Code verification request sent'); + }) + .catch((error) => alert.error(getErrorMessage(error))); + }; + + return ( + +
+ + + + + + + + + + + + + + + + + +