testnet-onboarding-app/src/pages/Validator.tsx

262 lines
8.8 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router-dom';
import { Box, Link, MenuItem, Select, TextField, Typography } from '@mui/material';
import { MsgCreateValidator } from 'cosmjs-types/cosmos/staking/v1beta1/tx';
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 {
enqueueSnackbar("Participant not found", { variant: "error" });
setParticipant(null);
}
} catch (error) {
console.error("Error fetching participant", 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) {
enqueueSnackbar("Transaction not sent", { variant: "error" });
} 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;