Add functionality to create a validator #28

Merged
nabarun merged 14 commits from deep-stack/testnet-onboarding-app:ag-validator-ui into main 2024-08-09 10:18:14 +00:00
16 changed files with 2053 additions and 1746 deletions

View File

@ -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

View File

@ -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",

View File

@ -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>

View File

@ -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 (

View File

@ -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';

View File

@ -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 (

View File

@ -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();

View File

@ -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>

View File

@ -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) {

View File

@ -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;

View File

@ -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({

View File

@ -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
View 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&nbsp;
<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;

View 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&nbsp;
<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
View File

@ -0,0 +1,6 @@
export interface Participant {
cosmosAddress: string;
nitroAddress: string;
role: string;
kycId: string;
}

3360
yarn.lock

File diff suppressed because it is too large Load Diff