Add functionality to create a validator (#28)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675) Co-authored-by: Adw8 <adwaitgharpure@gmail.com> Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com> Co-authored-by: IshaVenikar <ishavenikar7@gmail.com> Co-authored-by: Shreerang Kale <shreerangkale@gmail.com> Reviewed-on: #28
This commit is contained in:
parent
fc1c8df06b
commit
8ba837b2f4
@ -3,7 +3,8 @@ REACT_APP_ETHEREUM_MAINNET_CHAIN_ID=1
|
|||||||
REACT_APP_LACONICD_CHAIN_ID=laconic_9000-1
|
REACT_APP_LACONICD_CHAIN_ID=laconic_9000-1
|
||||||
REACT_APP_REGISTRY_GQL_ENDPOINT=http://localhost:9473/api
|
REACT_APP_REGISTRY_GQL_ENDPOINT=http://localhost:9473/api
|
||||||
REACT_APP_LACONICD_RPC_ENDPOINT=http://localhost:26657
|
REACT_APP_LACONICD_RPC_ENDPOINT=http://localhost:26657
|
||||||
REACT_APP_LACONICD_DENOM=alnt
|
|
||||||
REACT_APP_FAUCET_ENDPOINT=http://localhost:4000
|
REACT_APP_FAUCET_ENDPOINT=http://localhost:4000
|
||||||
REACT_APP_WALLET_META_URL=http://localhost:3000
|
REACT_APP_WALLET_META_URL=http://localhost:3000
|
||||||
REACT_APP_SUMSUB_API_ENDPOINT=
|
REACT_APP_SUMSUB_API_ENDPOINT=
|
||||||
|
REACT_APP_STAKING_AMOUNT=1000000000000000
|
||||||
|
REACT_APP_LACONICD_DENOM=alnt
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cerc-io/registry-sdk": "^0.2.5",
|
"@cerc-io/registry-sdk": "^0.2.5",
|
||||||
|
"@cosmjs/encoding": "^0.32.4",
|
||||||
|
"@cosmjs/proto-signing": "^0.32.4",
|
||||||
"@cosmjs/stargate": "^0.32.4",
|
"@cosmjs/stargate": "^0.32.4",
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
|
12
src/App.tsx
12
src/App.tsx
@ -14,6 +14,8 @@ import { WalletConnectProvider } from "./context/WalletConnectContext";
|
|||||||
import VerifyEmail from "./pages/VerifyEmail";
|
import VerifyEmail from "./pages/VerifyEmail";
|
||||||
import Email from "./pages/Email";
|
import Email from "./pages/Email";
|
||||||
import Thanks from "./pages/Thanks";
|
import Thanks from "./pages/Thanks";
|
||||||
|
import Validator from "./pages/Validator";
|
||||||
|
import ValidatorSuccess from "./pages/ValidatorSuccess";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -39,7 +41,15 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="/onboarding-success"
|
path="/onboarding-success"
|
||||||
element={<OnboardingSuccess />}
|
element={<OnboardingSuccess />}
|
||||||
></Route>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/validator"
|
||||||
|
element={<Validator />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/validator-success"
|
||||||
|
element={<ValidatorSuccess />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<PageNotFound />} />
|
<Route path="*" element={<PageNotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@ -6,11 +6,20 @@ import { Typography } from '@mui/material';
|
|||||||
// https://github.com/wojtekmaj/react-pdf?tab=readme-ov-file#copy-worker-to-public-directory
|
// https://github.com/wojtekmaj/react-pdf?tab=readme-ov-file#copy-worker-to-public-directory
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = process.env.PUBLIC_URL + '/pdf.worker.min.mjs';
|
pdfjs.GlobalWorkerOptions.workerSrc = process.env.PUBLIC_URL + '/pdf.worker.min.mjs';
|
||||||
|
|
||||||
const TermsAndConditionsBox = ({height}: {height: string}) => {
|
interface TermsAndConditionsBoxProps {
|
||||||
|
height: string;
|
||||||
|
onLoad?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TermsAndConditionsBox = ({ height, onLoad }: TermsAndConditionsBoxProps ) => {
|
||||||
const [numPages, setNumPages] = useState<number>();
|
const [numPages, setNumPages] = useState<number>();
|
||||||
|
|
||||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
||||||
setNumPages(numPages);
|
setNumPages(numPages);
|
||||||
|
|
||||||
|
if (onLoad){
|
||||||
|
onLoad();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -4,4 +4,4 @@ export const REDIRECT_EMAIL_MSG = 'Close this tab and the confirmation link in y
|
|||||||
|
|
||||||
export const ENABLE_KYC = false;
|
export const ENABLE_KYC = false;
|
||||||
|
|
||||||
export const SUBSCRIBER_ID_HASH_KEY = 'subscriberIdHash';
|
export const HASHED_SUBSCRIBER_ID_KEY = 'subscriberIdHash';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Outlet, useNavigate } from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Toolbar,
|
Toolbar,
|
||||||
@ -14,10 +14,14 @@ import { useWalletConnectContext } from "../context/WalletConnectContext";
|
|||||||
const SignPageLayout = () => {
|
const SignPageLayout = () => {
|
||||||
const { disconnect, session } = useWalletConnectContext();
|
const { disconnect, session } = useWalletConnectContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const disconnectHandler = async () => {
|
const disconnectHandler = async () => {
|
||||||
|
const { pathname } = location;
|
||||||
|
const redirectTo = pathname ? pathname.substring(1) : "";
|
||||||
|
|
||||||
await disconnect();
|
await disconnect();
|
||||||
navigate("/");
|
navigate(`/connect-wallet?redirectTo=${redirectTo}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import {useLocation, useNavigate } from "react-router-dom";
|
import {useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { Button, Box, Container, Typography, colors } from "@mui/material";
|
import { Button, Box, Container, Typography, colors } from "@mui/material";
|
||||||
|
|
||||||
@ -7,19 +7,37 @@ import { useWalletConnectContext } from "../context/WalletConnectContext";
|
|||||||
import { WALLET_DISCLAIMER_MSG } from "../constants";
|
import { WALLET_DISCLAIMER_MSG } from "../constants";
|
||||||
|
|
||||||
const ConnectWallet = () => {
|
const ConnectWallet = () => {
|
||||||
const { connect, session } = useWalletConnectContext();
|
const { connect, session, signClient, checkPersistedState } = useWalletConnectContext();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const redirectTo = searchParams.get("redirectTo");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (signClient && !session) {
|
||||||
|
checkPersistedState(signClient);
|
||||||
|
}
|
||||||
|
}, [checkPersistedState, signClient, session]);
|
||||||
|
|
||||||
if (session) {
|
useEffect(() => {
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectTo) {
|
||||||
|
navigate(`/${redirectTo}`, {
|
||||||
|
state: location.state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
navigate("/sign-with-nitro-key", {
|
navigate("/sign-with-nitro-key", {
|
||||||
state: location.state
|
state: location.state
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [session, navigate, location]);
|
}, [session, navigate, redirectTo, location.state]);
|
||||||
|
|
||||||
const handler = async () => {
|
const handler = async () => {
|
||||||
await connect();
|
await connect();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Box, Typography } from '@mui/material';
|
import { Button, Box, Typography } from '@mui/material';
|
||||||
@ -8,6 +8,8 @@ import TermsAndConditionsBox from '../components/TermsAndConditionsBox';
|
|||||||
const LandingPage = () => {
|
const LandingPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [isDisabled, setIsDisabled] = useState(true);
|
||||||
|
|
||||||
const handleAccept = () => {
|
const handleAccept = () => {
|
||||||
navigate('/verify-email');
|
navigate('/verify-email');
|
||||||
};
|
};
|
||||||
@ -34,9 +36,9 @@ const LandingPage = () => {
|
|||||||
those same validators can complete their service provider setup. Once service providers are live, app publishers can start deploying webapps to individual service providers.
|
those same validators can complete their service provider setup. Once service providers are live, app publishers can start deploying webapps to individual service providers.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<TermsAndConditionsBox height="43vh" />
|
<TermsAndConditionsBox height="43vh" onLoad={()=>{setIsDisabled(false);}} />
|
||||||
<Box mt={2} display="flex" justifyContent="center">
|
<Box mt={2} display="flex" justifyContent="center">
|
||||||
<Button variant="contained" color="primary" onClick={handleAccept}>
|
<Button variant="contained" color="primary" onClick={handleAccept} disabled={isDisabled}>
|
||||||
Accept
|
Accept
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -8,14 +8,8 @@ import SumsubWebSdk from "@sumsub/websdk-react";
|
|||||||
import { MessageHandler } from "@sumsub/websdk";
|
import { MessageHandler } from "@sumsub/websdk";
|
||||||
|
|
||||||
import { config, fetchAccessToken, getAccessTokenExpirationHandler, options } from "../utils/sumsub";
|
import { config, fetchAccessToken, getAccessTokenExpirationHandler, options } from "../utils/sumsub";
|
||||||
import { ENABLE_KYC, SUBSCRIBER_ID_HASH_KEY } from "../constants";
|
import { ENABLE_KYC, HASHED_SUBSCRIBER_ID_KEY } from "../constants";
|
||||||
|
import { Participant } from "../types";
|
||||||
interface Participant {
|
|
||||||
cosmosAddress: string;
|
|
||||||
nitroAddress: string;
|
|
||||||
role: string;
|
|
||||||
kycId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const registry = new Registry(
|
const registry = new Registry(
|
||||||
process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!
|
process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!
|
||||||
@ -48,7 +42,7 @@ const OnboardingSuccess = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.removeItem(SUBSCRIBER_ID_HASH_KEY);
|
localStorage.removeItem(HASHED_SUBSCRIBER_ID_KEY);
|
||||||
|
|
||||||
setParticipant(participant);
|
setParticipant(participant);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -12,7 +12,7 @@ import { StargateClient } from "@cosmjs/stargate";
|
|||||||
|
|
||||||
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
||||||
import SelectRoleCard, {Role} from "../components/SelectRoleCard";
|
import SelectRoleCard, {Role} from "../components/SelectRoleCard";
|
||||||
import { SUBSCRIBER_ID_HASH_KEY } from "../constants";
|
import { HASHED_SUBSCRIBER_ID_KEY } from "../constants";
|
||||||
|
|
||||||
const SignWithCosmos = () => {
|
const SignWithCosmos = () => {
|
||||||
const { session, signClient } = useWalletConnectContext();
|
const { session, signClient } = useWalletConnectContext();
|
||||||
@ -36,7 +36,7 @@ const SignWithCosmos = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ethAddress = innerMessage!.address;
|
const ethAddress = innerMessage!.address;
|
||||||
const subscriberIdHash = localStorage.getItem(SUBSCRIBER_ID_HASH_KEY);
|
const subscriberIdHash = localStorage.getItem(HASHED_SUBSCRIBER_ID_KEY);
|
||||||
|
|
||||||
const createCosmosClient = useCallback(async (endpoint: string) => {
|
const createCosmosClient = useCallback(async (endpoint: string) => {
|
||||||
return await StargateClient.connect(endpoint);
|
return await StargateClient.connect(endpoint);
|
||||||
@ -100,6 +100,8 @@ const SignWithCosmos = () => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
enqueueSnackbar("View and sign the message from your Laconic Wallet", { variant: "info" });
|
||||||
|
|
||||||
const params = { transactionMessage, signer: cosmosAddress };
|
const params = { transactionMessage, signer: cosmosAddress };
|
||||||
const responseFromWallet = await signClient!.request<{
|
const responseFromWallet = await signClient!.request<{
|
||||||
code: number;
|
code: number;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useMemo, useEffect } from "react";
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import canonicalStringify from "canonical-json";
|
import canonicalStringify from "canonical-json";
|
||||||
|
|
||||||
@ -13,27 +13,34 @@ import LoadingButton from '@mui/lab/LoadingButton';
|
|||||||
import { utf8ToHex } from "@walletconnect/encoding";
|
import { utf8ToHex } from "@walletconnect/encoding";
|
||||||
|
|
||||||
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
||||||
import { ENABLE_KYC, SUBSCRIBER_ID_HASH_KEY } from "../constants";
|
import { ENABLE_KYC, HASHED_SUBSCRIBER_ID_KEY } from "../constants";
|
||||||
|
|
||||||
const SignWithNitroKey = () => {
|
const SignWithNitroKey = () => {
|
||||||
|
|
||||||
const { session, signClient, checkPersistedState } =
|
const { session, signClient } =
|
||||||
useWalletConnectContext();
|
useWalletConnectContext();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (signClient && !session) {
|
|
||||||
checkPersistedState(signClient);
|
|
||||||
}
|
|
||||||
}, [session, signClient, checkPersistedState]);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session) {
|
||||||
|
navigate("/connect-wallet?redirectTo=sign-with-nitro-key", {
|
||||||
|
state: location.state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [session, navigate, location.state]);
|
||||||
|
|
||||||
const [ethAddress, setEthAddress] = useState("");
|
const [ethAddress, setEthAddress] = useState("");
|
||||||
const [ethSignature, setEthSignature] = useState("");
|
const [ethSignature, setEthSignature] = useState("");
|
||||||
|
|
||||||
const [cosmosAddress, setCosmosAddress] = useState("");
|
const [cosmosAddress, setCosmosAddress] = useState("");
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const subscriberIdHash = localStorage.getItem(SUBSCRIBER_ID_HASH_KEY);
|
|
||||||
|
const subscriberIdHash = useMemo(()=>{
|
||||||
|
return localStorage.getItem(HASHED_SUBSCRIBER_ID_KEY);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!subscriberIdHash) {
|
if (!subscriberIdHash) {
|
||||||
@ -53,6 +60,9 @@ const SignWithNitroKey = () => {
|
|||||||
if (session && signClient) {
|
if (session && signClient) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
enqueueSnackbar("View and sign the message from your Laconic Wallet", { variant: "info" });
|
||||||
|
|
||||||
const jsonMessage = canonicalStringify(message);
|
const jsonMessage = canonicalStringify(message);
|
||||||
const hexMsg = utf8ToHex(jsonMessage, true);
|
const hexMsg = utf8ToHex(jsonMessage, true);
|
||||||
const receivedEthSig: string = await signClient!.request({
|
const receivedEthSig: string = await signClient!.request({
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from 'jwt-decode';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
import { Box, colors, Typography } from '@mui/material';
|
import { Box, colors, Typography } from '@mui/material';
|
||||||
|
|
||||||
import { SUBSCRIBER_ID_HASH_KEY } from '../constants';
|
import { HASHED_SUBSCRIBER_ID_KEY } from '../constants';
|
||||||
|
|
||||||
interface JwtPayload {
|
interface JwtPayload {
|
||||||
subscriber_id: string;
|
subscriber_id: string;
|
||||||
@ -25,7 +25,7 @@ const Thanks: React.FC = () => {
|
|||||||
const token = queryParams.get('jwt_token');
|
const token = queryParams.get('jwt_token');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(!token){
|
if (!token) {
|
||||||
throw new Error("Invalid JWT Token");
|
throw new Error("Invalid JWT Token");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,9 +43,9 @@ const Thanks: React.FC = () => {
|
|||||||
const subscriberIdBytes = ethers.utils.toUtf8Bytes(decoded.subscriber_id);
|
const subscriberIdBytes = ethers.utils.toUtf8Bytes(decoded.subscriber_id);
|
||||||
const subscriberIdHash = ethers.utils.sha256(subscriberIdBytes);
|
const subscriberIdHash = ethers.utils.sha256(subscriberIdBytes);
|
||||||
|
|
||||||
localStorage.setItem(SUBSCRIBER_ID_HASH_KEY, subscriberIdHash);
|
localStorage.setItem(HASHED_SUBSCRIBER_ID_KEY, subscriberIdHash);
|
||||||
|
|
||||||
navigate('/connect-wallet');
|
navigate('/sign-with-nitro-key');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErr(String(error));
|
setErr(String(error));
|
||||||
}
|
}
|
||||||
|
261
src/pages/Validator.tsx
Normal file
261
src/pages/Validator.tsx
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { MsgCreateValidator } from 'cosmjs-types/cosmos/staking/v1beta1/tx';
|
||||||
|
|
||||||
|
import { Box, Link, MenuItem, Select, TextField, Typography } from '@mui/material';
|
||||||
|
import { fromBech32, toBech32 } from '@cosmjs/encoding';
|
||||||
|
import { LoadingButton } from '@mui/lab';
|
||||||
|
import { EncodeObject, encodePubkey } from '@cosmjs/proto-signing';
|
||||||
|
import { Registry } from '@cerc-io/registry-sdk';
|
||||||
|
|
||||||
|
import { useWalletConnectContext } from '../context/WalletConnectContext';
|
||||||
|
import { Participant } from '../types';
|
||||||
|
|
||||||
|
const Validator = () => {
|
||||||
|
const { session, signClient } = useWalletConnectContext();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [cosmosAddress, setCosmosAddress] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [moniker, setMoniker] = useState('');
|
||||||
|
const [pubKey, setPubKey] = useState('');
|
||||||
|
const [participant, setParticipant] = useState<Participant | null>(null);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session) {
|
||||||
|
navigate("/connect-wallet?redirectTo=validator");
|
||||||
|
}
|
||||||
|
}, [session, navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cosmosAddress) {
|
||||||
|
setParticipant(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchParticipant = async () => {
|
||||||
|
const registry = new Registry(process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetchedParticipant = await registry.getParticipantByAddress(cosmosAddress);
|
||||||
|
if (fetchedParticipant) {
|
||||||
|
setParticipant(fetchedParticipant);
|
||||||
|
} else {
|
||||||
|
throw new Error("Participant not found");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching participant", error);
|
||||||
|
enqueueSnackbar("Error in fetching participant", { variant: "error" });
|
||||||
|
setParticipant(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchParticipant();
|
||||||
|
}, [cosmosAddress]);
|
||||||
|
|
||||||
|
const isMonikerValid = useMemo(() => moniker.trim().length > 0, [moniker]);
|
||||||
|
const isPubKeyValid = useMemo(() => pubKey.length === 44, [pubKey]);
|
||||||
|
|
||||||
|
const msgCreateValidator: MsgCreateValidator = useMemo(() => {
|
||||||
|
const encodedPubKey = encodePubkey({
|
||||||
|
type: "tendermint/PubKeyEd25519",
|
||||||
|
value: pubKey.length === 44 ? pubKey : '',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: {
|
||||||
|
moniker,
|
||||||
|
identity: "",
|
||||||
|
website: "",
|
||||||
|
securityContact: "",
|
||||||
|
details: "",
|
||||||
|
},
|
||||||
|
commission: {
|
||||||
|
maxChangeRate: "10000000000000000", // 0.01
|
||||||
|
maxRate: "200000000000000000", // 0.2
|
||||||
|
rate: "100000000000000000", // 0.1
|
||||||
|
},
|
||||||
|
minSelfDelegation: "1",
|
||||||
|
delegatorAddress: '',
|
||||||
|
validatorAddress: cosmosAddress && toBech32('laconicvaloper', fromBech32(cosmosAddress).data),
|
||||||
|
pubkey: encodedPubKey,
|
||||||
|
value: {
|
||||||
|
amount: process.env.REACT_APP_STAKING_AMOUNT!,
|
||||||
|
denom: process.env.REACT_APP_LACONICD_DENOM!,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [cosmosAddress, pubKey, moniker]);
|
||||||
|
|
||||||
|
const msgCreateValidatorEncodeObject: EncodeObject = {
|
||||||
|
typeUrl: '/cosmos.staking.v1beta1.MsgCreateValidator',
|
||||||
|
value: MsgCreateValidator.toJSON(msgCreateValidator),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendTransaction = async () => {
|
||||||
|
if (
|
||||||
|
!isMonikerValid ||
|
||||||
|
!isPubKeyValid ||
|
||||||
|
!msgCreateValidator.validatorAddress
|
||||||
|
) {
|
||||||
|
setIsError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
enqueueSnackbar("View and sign the message from your Laconic Wallet", { variant: "info" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = { transactionMessage: msgCreateValidatorEncodeObject, signer: cosmosAddress };
|
||||||
|
const response = await signClient!.request<{ code: number }>({
|
||||||
|
topic: session!.topic,
|
||||||
|
chainId: `cosmos:${process.env.REACT_APP_LACONICD_CHAIN_ID}`,
|
||||||
|
request: {
|
||||||
|
method: "cosmos_sendTransaction",
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code !== 0) {
|
||||||
|
throw new Error("Transaction not sent");
|
||||||
|
} else {
|
||||||
|
navigate("/validator-success", { state: { validatorAddress: msgCreateValidator.validatorAddress, } });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending transaction", error);
|
||||||
|
enqueueSnackbar("Error in sending transaction", { variant: "error" });
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const replacer = (key: string, value: any): any => {
|
||||||
|
if (value instanceof Uint8Array) {
|
||||||
|
return Buffer.from(value).toString('hex');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", marginTop: 6, gap: 1 }}>
|
||||||
|
<Typography variant="h5">Create a validator</Typography>
|
||||||
|
<Typography variant="body1">Select Laconic account:</Typography>
|
||||||
|
<Select
|
||||||
|
sx={{ marginBottom: 2 }}
|
||||||
|
id="cosmos-address-select"
|
||||||
|
value={cosmosAddress}
|
||||||
|
onChange={(e) => setCosmosAddress(e.target.value)}
|
||||||
|
style={{ maxWidth: "600px", display: "block" }}
|
||||||
|
>
|
||||||
|
{session?.namespaces.cosmos.accounts.map((address, index) => (
|
||||||
|
<MenuItem value={address.split(":")[2]} key={index}>
|
||||||
|
{address.split(":")[2]}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{Boolean(cosmosAddress) && (
|
||||||
|
<>
|
||||||
|
{participant ? (
|
||||||
|
<Typography>Onboarded participant</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography>No participant found</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: participant ? "lightgray" : "white",
|
||||||
|
padding: 3,
|
||||||
|
wordWrap: "break-word",
|
||||||
|
marginBottom: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{participant && (
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
|
||||||
|
Cosmos Address: {participant.cosmosAddress} <br />
|
||||||
|
Nitro Address: {participant.nitroAddress} <br />
|
||||||
|
Role: {participant.role} <br />
|
||||||
|
KYC ID: {participant.kycId} <br />
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{participant?.role === "validator" && (
|
||||||
|
<>
|
||||||
|
<Box style={{ maxWidth: "600px" }}>
|
||||||
|
<TextField
|
||||||
|
id="moniker"
|
||||||
|
label="Enter your node moniker (example: AliceNode)"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={moniker}
|
||||||
|
onChange={(e) => {
|
||||||
|
setIsError(false);
|
||||||
|
setMoniker(e.target.value);
|
||||||
|
}}
|
||||||
|
error={!isMonikerValid && isError}
|
||||||
|
helperText={!isMonikerValid && isError ? "Moniker is required" : ""}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography sx={{ marginTop: 3}}>
|
||||||
|
Fetch your validator public key using the following command (refer
|
||||||
|
<Link
|
||||||
|
href="https://git.vdb.to/cerc-io/testnet-laconicd-stack/src/branch/main/testnet-onboarding-validator.md#join-as-testnet-validator"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
this guide
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ backgroundColor: "lightgray", padding: 3, wordWrap: "break-word" }}>
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
|
||||||
|
{`laconic-so deployment --dir testnet-laconicd-deployment exec laconicd "laconicd cometbft show-validator" | jq -r .key`}
|
||||||
|
</pre>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ maxWidth: "600px" }}>
|
||||||
|
<TextField
|
||||||
|
id="pub-key"
|
||||||
|
label="Enter your validator public key"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={pubKey}
|
||||||
|
onChange={(e) => {
|
||||||
|
setIsError(false);
|
||||||
|
setPubKey(e.target.value);
|
||||||
|
}}
|
||||||
|
error={!isPubKeyValid && isError}
|
||||||
|
helperText={!isPubKeyValid && isError ? "Public key must be 44 characters" : ""}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography>Send transaction to chain</Typography>
|
||||||
|
<Box sx={{ backgroundColor: "lightgray", padding: 3, wordWrap: "break-word" }}>
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
|
||||||
|
{JSON.stringify(msgCreateValidator, replacer, 2)}
|
||||||
|
</pre>
|
||||||
|
</Box>
|
||||||
|
<Box marginTop={1} marginBottom={1}>
|
||||||
|
<LoadingButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={sendTransaction}
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={isError}
|
||||||
|
>
|
||||||
|
Send transaction
|
||||||
|
</LoadingButton>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Validator;
|
42
src/pages/ValidatorSuccess.tsx
Normal file
42
src/pages/ValidatorSuccess.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Box, Link, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
const ValidatorSuccess = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const { validatorAddress } = location.state as {
|
||||||
|
validatorAddress?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
marginTop: 6,
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Validator created successfully</Typography>
|
||||||
|
<Typography sx={{ marginTop: 3}}>
|
||||||
|
You can view your validator details using the following command (Refer
|
||||||
|
<Link
|
||||||
|
href="https://git.vdb.to/cerc-io/testnet-laconicd-stack/src/branch/main/testnet-onboarding-validator.md#join-as-testnet-validator"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
this guide
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ backgroundColor: "lightgray", padding: 2, wordWrap: "break-word", marginTop: 2, fontSize: 14}}>
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
|
||||||
|
{`laconic-so deployment --dir testnet-laconicd-deployment exec laconicd "laconicd query staking validators --output json" | jq '.validators[] | select(.operator_address == "${validatorAddress}")'`}
|
||||||
|
</pre>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ValidatorSuccess;
|
6
src/types.ts
Normal file
6
src/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface Participant {
|
||||||
|
cosmosAddress: string;
|
||||||
|
nitroAddress: string;
|
||||||
|
role: string;
|
||||||
|
kycId: string;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user