diff --git a/.env b/.env index aa925c7..8d77d23 100644 --- a/.env +++ b/.env @@ -2,10 +2,8 @@ VITE_SHOW_MANUAL_VERIFICATION=true VITE_VERIFIER_ID=orbs.com VITE_BACKEND_URL=https://ton-source-prod-1.herokuapp.com,https://ton-source-prod-2.herokuapp.com,https://ton-source-prod-3.herokuapp.com -VITE_VERIFIER_REGISTRY=EQDS0AW7NV1w3nFwx-mmryfpH4OGQ3PXnoFGOJA_8PTHuLrw VITE_SOURCES_REGISTRY=EQD-BJSVUJviud_Qv7Ymfd3qzXdrmV525e3YDzWQoHIAiInL VITE_VERIFIER_ID_TESTNET=orbs-testnet VITE_BACKEND_URL_TESTNET=https://ton-source-prod-testnet-1.herokuapp.com -VITE_VERIFIER_REGISTRY_TESTNET=EQB--CRXUbqYbqJKScEWeOrnk0TKJxB071M-WwvMpMEvq6Ed VITE_SOURCES_REGISTRY_TESTNET=EQCsdKYwUaXkgJkz2l0ol6qT_WxeRbE_wBCwnEybmR0u5TO8 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ca579ea..8ebde27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-ga4": "^1.4.1", + "react-hook-form": "^7.48.2", "react-qr-code": "^2.0.8", "react-router-dom": "^6.4.2", "source-map-explorer": "^2.5.3", @@ -3544,6 +3545,21 @@ "resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-1.4.1.tgz", "integrity": "sha512-ioBMEIxd4ePw4YtaloTUgqhQGqz5ebDdC4slEpLgy2sLx1LuZBC9iYCwDymTXzcntw6K1dHX183ulP32nNdG7w==" }, + "node_modules/react-hook-form": { + "version": "7.48.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.48.2.tgz", + "integrity": "sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -6871,6 +6887,12 @@ "resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-1.4.1.tgz", "integrity": "sha512-ioBMEIxd4ePw4YtaloTUgqhQGqz5ebDdC4slEpLgy2sLx1LuZBC9iYCwDymTXzcntw6K1dHX183ulP32nNdG7w==" }, + "react-hook-form": { + "version": "7.48.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.48.2.tgz", + "integrity": "sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==", + "requires": {} + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/package.json b/package.json index b82199f..1ea9807 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,13 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-ga4": "^1.4.1", + "react-hook-form": "^7.48.2", "react-qr-code": "^2.0.8", "react-router-dom": "^6.4.2", "source-map-explorer": "^2.5.3", - "tvm-disassembler": "^2.0.2", "ton": "^13.4.1", "ton-core": "^0.49.0", + "tvm-disassembler": "^2.0.2", "web-tree-sitter": "^0.20.7", "zustand": "^4.1.3" }, diff --git a/src/components/admin/Admin.tsx b/src/components/admin/Admin.tsx index 077dbab..ed77aec 100644 --- a/src/components/admin/Admin.tsx +++ b/src/components/admin/Admin.tsx @@ -1,23 +1,19 @@ -import { Box } from "@mui/material"; +import { Box, Stack } from "@mui/material"; import { TestnetBar } from "../TestnetBar"; import SourcesRegistry from "./SourcesRegistry"; import { VerifierRegistry } from "./VerifierRegistry"; import { FlexBoxRow } from "../Getters.styled"; import { Footer } from "../Footer"; -import ConnectButton from "../ConnectButton"; +import { StyledTonConnectButton } from "../../styles"; export function Admin() { return (
{window.isTestnet && } - +

Admin

- -
+ +
+ ); } diff --git a/src/components/admin/VerifierRegistry.tsx b/src/components/admin/VerifierRegistry.tsx index 08f5467..b70f130 100644 --- a/src/components/admin/VerifierRegistry.tsx +++ b/src/components/admin/VerifierRegistry.tsx @@ -1,13 +1,17 @@ import InfoPiece from "../InfoPiece"; import { useLoadVerifierRegistryInfo } from "../../lib/useLoadVerifierRegistryInfo"; -import { Dictionary, beginCell, toNano, DictionaryValue, Slice } from "ton"; +import { Dictionary, beginCell, toNano, DictionaryValue, Slice, Address } from "ton"; import { toBigIntBE } from "bigint-buffer"; -import { useState } from "react"; -import { Dialog, DialogTitle, DialogContent, TextField, DialogActions } from "@mui/material"; +import { useMemo } from "react"; +import { Stack, Grid, CircularProgress, Alert } from "@mui/material"; import Button from "../Button"; import { toSha256Buffer } from "../../lib/useLoadContractProof"; import { useRequestTXN } from "../../hooks"; import { Verifier } from "../../lib/wrappers/verifier-registry"; +import { useFieldArray, useForm } from "react-hook-form"; +import { TextField } from "./form/TextField"; +import { useTonConnectUI } from "@tonconnect/ui-react"; +import { useLoadSourcesRegistryInfo } from "../../lib/useLoadSourcesRegistryInfo"; export const OperationCodes = { removeVerifier: 0x19fa5637, @@ -35,6 +39,13 @@ function createSliceValue(): DictionaryValue { }; } +type VerifierRegistryForm = { + quorum: string; + name: string; + url: string; + pubKeyEndpoints: { pubKey: string; ip: string }[]; +}; + function updateVerifier(params: { queryId?: number; id: bigint; @@ -61,121 +72,203 @@ function updateVerifier(params: { return msgBody.endCell(); } -function UpdateVerifier({ verifier }: { verifier: Verifier }) { - const [open, setOpen] = useState(false); +function removeVerifier(params: { queryId?: number; id: bigint }) { + let msgBody = beginCell(); + msgBody.storeUint(OperationCodes.removeVerifier, 32); + msgBody.storeUint(params.queryId || 0, 64); + msgBody.storeUint(params.id, 256); + return msgBody.endCell(); +} + +function VerifierRegsitryForm({ + verifier, + altColor, + isNew, +}: { + verifier: Verifier; + altColor: boolean; + isNew: boolean; +}) { const requestTXN = useRequestTXN(); - const { data, isLoading } = useLoadVerifierRegistryInfo(); + const { data: sourcesRegistry } = useLoadSourcesRegistryInfo(); - const [value, setValue] = useState( - JSON.stringify( - { - quorum: verifier.quorum, - pubKeyEndpoints: verifier.pubKeyEndpoints, - name: verifier.name, - url: verifier.url, - }, - null, - 3, - ), + const defaultPubKeyEndpoints = useMemo( + () => + Object.entries(verifier.pubKeyEndpoints).map(([pubKey, ip]) => ({ + pubKey, + ip, + })), + [verifier.pubKeyEndpoints], ); + const form = useForm({ + defaultValues: { + quorum: verifier.quorum.toString() || "", + name: verifier.name || "", + url: verifier.url || "", + pubKeyEndpoints: defaultPubKeyEndpoints || [], + }, + mode: "onChange", + }); + + async function onSubmit(values: VerifierRegistryForm) { + // validate values + if (!values.name) { + form.setError("name", { message: "Name is required" }); + return; + } + + if (!values.url) { + form.setError("url", { message: "Url is required" }); + return; + } + + if (!values.quorum || Number(values.quorum) < 1) { + form.setError("quorum", { message: "Quorum is required and should be at least 1" }); + return; + } + + try { + const result = await requestTXN( + sourcesRegistry?.verifierRegistry ?? "", + toNano(isNew ? "1000" : "0.01"), + updateVerifier({ + id: sha256BN(values.name), + quorum: Number(values.quorum), + endpoints: new Map( + values.pubKeyEndpoints.map(({ pubKey, ip }) => [ + toBigIntBE(Buffer.from(pubKey, "base64")), + ip2num(ip), + ]), + ), + name: values.name, + marketingUrl: values.url, + }), + ); + if (result === "rejected") { + form.setError("root", { message: `Failed to update config of ${values.name}` }); + } + } catch (err) { + let errMessage = `Failed to update config of ${values.name}`; + + if ("message" in (err as Error)) { + errMessage = (err as Error).message; + } + + form.setError("root", { message: errMessage }); + } + } + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "pubKeyEndpoints", + }); + return ( - <> -