Compare commits

...

18 Commits

Author SHA1 Message Date
IshaVenikar
e71d813a4a Use laconic namespace 2024-08-14 18:29:42 +05:30
IshaVenikar
9e34835adf Update registry-sdk version 2024-08-14 14:54:15 +05:30
IshaVenikar
5774c7b32d Replace cosmos_address with laconic_address 2024-08-14 13:10:50 +05:30
eba6cd5d68 Merge pull request 'Skin onboarding app' (#33) from style/vaidator into main
Reviewed-on: cerc-io/testnet-onboarding-app#33
2024-08-11 21:05:04 +00:00
zramsay
5baccf3a84 bump version 2024-08-11 17:02:20 -04:00
b39afe386f style: validator success page 2024-08-11 12:34:35 -04:00
565d1887e0 style: add codeblock and style validator page 2024-08-11 12:32:55 -04:00
zramsay
e6fa6aabd7 copy: fix role names 2024-08-11 15:53:43 +05:30
3fe0576f34 style: add back t&c text and style pre block 2024-08-11 15:30:40 +05:30
Monkey
31e3b75dfa remove width setting for terms and conditions 2024-08-11 15:29:59 +05:30
bee7379e86 style: sign with cosmos 2024-08-11 15:29:58 +05:30
bff5ab9f31 style: sign with nitro 2024-08-11 15:28:14 +05:30
051de43480 chore: add dm mono 2024-08-11 15:28:14 +05:30
ad614aff2f style: remove disclaimer and center button 2024-08-11 15:28:14 +05:30
c038085b87 style: first pass with dark mui theme 2024-08-11 15:28:09 +05:30
09104b50bf Add download button for terms and conditions PDF (#31)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

- Replace Cosmos address with Laconic address

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/testnet-onboarding-app#31
2024-08-11 09:47:18 +00:00
663eb42a74 Wait for session to load before redirecting (#29)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/testnet-onboarding-app#29
2024-08-09 13:00:30 +00:00
8ba837b2f4 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: cerc-io/testnet-onboarding-app#28
2024-08-09 10:18:13 +00:00
27 changed files with 2824 additions and 2235 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

@ -1,5 +1,5 @@
# testnet-onboarding-app # testnet-onboarding-app
React app for onboarding participants to laconicd chain with Nitro/Cosmos key attestation React app for onboarding participants to laconicd chain with Nitro/Laconic key attestation
## Setup for testnet-onboarding-app ## Setup for testnet-onboarding-app

View File

@ -1,9 +1,11 @@
{ {
"name": "testnet-onboarding-app", "name": "testnet-onboarding-app",
"version": "0.1.0", "version": "0.1.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@cerc-io/registry-sdk": "^0.2.5", "@cerc-io/registry-sdk": "^0.2.6",
"@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

@ -1,20 +1,23 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Testnet Onboarding App" />
name="description"
content="Testnet Onboarding App"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
rel="stylesheet" />
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
@ -61,8 +64,9 @@
} }
} }
</style> </style>
</head> </head>
<body>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"> <div id="root">
<div class="loader-wrapper"> <div class="loader-wrapper">
@ -79,5 +83,6 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
</body> </body>
</html> </html>

View File

@ -3,7 +3,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import ConnectWallet from "./pages/ConnectWallet"; import ConnectWallet from "./pages/ConnectWallet";
import SignWithNitroKey from "./pages/SignWithNitroKey"; import SignWithNitroKey from "./pages/SignWithNitroKey";
import SignWithCosmos from "./pages/SignWithCosmos"; import SignWithLaconic from "./pages/SignWithLaconic";
import PageNotFound from "./pages/PageNotFound"; import PageNotFound from "./pages/PageNotFound";
import OnboardingSuccess from "./pages/OnboardingSuccess"; import OnboardingSuccess from "./pages/OnboardingSuccess";
import SignPageLayout from "./layout/SignPageLayout"; import SignPageLayout from "./layout/SignPageLayout";
@ -14,36 +14,119 @@ 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";
import { createTheme, Box, ThemeProvider, CssBaseline } from "@mui/material";
const darkTheme = createTheme({
components: {
MuiAccordion: {
defaultProps: {
sx: {
border: "1px solid #48474F",
borderBottomRightRadius: 3,
borderBottomLeftRadius: 3,
},
},
},
MuiButton: {
defaultProps: {
color: "primary",
sx: {
fontFamily: `DM Mono, monospace`,
fontWeight: 400,
},
},
},
MuiLink: {
defaultProps: {
color: "text.primary",
fontSize: "14px",
},
},
MuiTypography: {
defaultProps: {
color: "text.primary",
fontWeight: 400,
},
},
MuiPaper: {
defaultProps: {
sx: {
backgroundImage: "none",
},
},
},
},
palette: {
mode: "dark",
primary: {
main: "#0000F4",
},
secondary: {
main: "#A2A2FF",
},
error: {
main: "#B20710",
},
background: {
default: "#0F0F0F",
paper: "#18181A",
},
text: {
primary: "#FBFBFB",
},
info: {
main: "#FBFBFB",
},
},
});
function App() { function App() {
return ( return (
<Router> <Router>
<ThemeProvider theme={darkTheme}>
<Box
sx={{
height: "100vh",
width: "100vw",
backgroundColor: "background.default",
}}
>
<Header /> <Header />
<CssBaseline />
<WalletConnectProvider> <WalletConnectProvider>
<Routes> <Routes>
<Route path="/" element={<LandingPage />} /> <Route path="/" element={<LandingPage />} />
<Route path="/verify-email" element={<VerifyEmail />} /> <Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/email" element={<Email/>} /> <Route path="/email" element={<Email />} />
<Route path="/connect-wallet" element={<ConnectWallet />} /> <Route path="/connect-wallet" element={<ConnectWallet />} />
<Route path="/thanks" element={<Thanks />} /> <Route path="/thanks" element={<Thanks />} />
<Route element={<SignPageLayout />}> <Route element={<SignPageLayout />}>
<Route path="/sign-with-nitro-key" element={<SignWithNitroKey />} /> <Route
path="/sign-with-nitro-key"
element={<SignWithNitroKey />}
/>
<Route <Route
path="/user-verification" path="/user-verification"
element={<UserVerification />} element={<UserVerification />}
/> />
<Route <Route path="/sign-with-laconic" element={<SignWithLaconic />} />
path="/sign-with-cosmos"
element={<SignWithCosmos />}
/>
<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>
</WalletConnectProvider> </WalletConnectProvider>
</Box>
</ThemeProvider>
</Router> </Router>
); );
} }

View File

@ -0,0 +1,25 @@
import { Box } from "@mui/material";
import React, { PropsWithChildren } from "react";
export const CodeBlock: React.FC<PropsWithChildren> = ({ children }) => (
<Box
sx={{
backgroundColor: "#48474F",
padding: 3,
wordWrap: "break-word",
mt: 1,
borderRadius: 1,
}}
>
<pre
style={{
whiteSpace: "pre-wrap",
margin: 0,
backgroundColor: "#48474F",
color: "#FBFBFB",
}}
>
{children}
</pre>
</Box>
);

View File

@ -0,0 +1,21 @@
import { Box, BoxProps } from "@mui/material";
import React, { PropsWithChildren } from "react";
export const Container: React.FC<
PropsWithChildren<{ boxProps?: BoxProps }>
> = ({ children, boxProps = {} }) => (
<Box
{...boxProps}
sx={{
width: "100%",
maxWidth: "752px",
mx: "auto",
backgroundColor: "background.paper",
padding: 3,
borderRadius: 2,
...boxProps.sx,
}}
>
{children}
</Box>
);

View File

@ -1,31 +1,90 @@
import React from 'react'; import React from "react";
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from "react-router-dom";
import { AppBar, Toolbar, Avatar, Box, IconButton } from '@mui/material'; import { AppBar, SvgIcon, Stack, Divider, Typography } from "@mui/material";
const Header: React.FC = () => { const Header: React.FC = () => {
const location = useLocation(); const location = useLocation();
return ( return (
<AppBar position="static" color="inherit"> <AppBar
<Toolbar> position="static"
<Link to={location.pathname === "/" ? "/" : "/connect-wallet"} style={{ color: "inherit", textDecoration: "none" }}>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Avatar
alt="Laconic logo"
src="https://avatars.githubusercontent.com/u/92608123"
/>
<IconButton
edge="start"
color="inherit" color="inherit"
aria-label="menu" sx={{ boxShadow: "none", mb: 4, height: 48 }}
sx={{ ml: 2, mr: 2 }}
> >
Testnet Onboarding <Stack
</IconButton> direction="row"
</Box> sx={{
backgroundColor: "background.paper",
pl: 2,
alignItems: "center",
py: 1,
}}
spacing={1}
>
<Link
to={location.pathname === "/" ? "/" : "/connect-wallet"}
style={{
color: "inherit",
textDecoration: "none",
display: "flex",
alignItems: "center",
marginRight: 4,
}}
>
<SvgIcon sx={{ height: 20, width: 100 }}>
<svg
width="115"
height="20"
viewBox="0 0 115 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.37388 10.5194C5.70149 8.19185 7.14225 4.97748 7.1416 1.42853C7.14246 0.94681 7.11586 0.470456 7.063 0L-0.000488281 0.000643078L-0.000273922 13.5723C-0.000917354 15.2174 0.62632 16.863 1.88091 18.1175C3.1356 19.3721 4.78235 20.0001 6.42772 19.9993L6.42729 19.9997L19.9995 20L19.999 12.9355C19.5296 12.8838 19.0532 12.857 18.5704 12.8569C15.0224 12.8574 11.8079 14.298 9.48026 16.6255C7.78654 18.2768 5.07093 18.2771 3.39812 16.6043C1.72638 14.9325 1.72562 12.2161 3.37388 10.5194ZM18.5344 1.46863C16.5837 -0.481929 13.4146 -0.48268 11.4633 1.46863C9.512 3.41984 9.51276 6.58895 11.4633 8.53941C13.415 10.491 16.5831 10.4907 18.5344 8.53941C20.4857 6.5882 20.4861 3.42016 18.5344 1.46863Z"
fill="#FBFBFB"
/>
<path
d="M31.4741 18.5838H39.2552V16.3302H34.075V1.41351H31.4741V18.5838Z"
fill="#FBFBFB"
/>
<path
d="M49.8108 1.41351H45.4976L40.9893 18.5838H43.6769L44.8039 14.2913H50.3744L51.5014 18.5838H54.3191L49.8108 1.41351ZM45.3458 12.145L47.6 3.2593H47.6866L49.8541 12.145H45.3458Z"
fill="#FBFBFB"
/>
<path
d="M62.9292 8.06885H65.9636C65.9636 3.17534 64.3813 1.07196 60.6967 1.07196C56.8169 1.07196 55.1479 3.73341 55.1479 9.97909C55.1479 16.2462 56.8169 18.9291 60.6967 18.9291C64.3813 18.9291 65.9636 16.8901 65.9853 12.1468H62.9508C62.9292 15.8599 62.474 16.7828 60.6967 16.7828C58.6593 16.7828 58.1607 15.4307 58.1824 9.97909C58.1824 4.54896 58.6809 3.19678 60.6967 3.21823C62.474 3.21823 62.9292 4.18413 62.9292 8.06885Z"
fill="#FBFBFB"
/>
<path
d="M73.7781 1.07209C77.7229 1.09364 79.4135 3.77643 79.4135 10.0007C79.4135 16.2249 77.7229 18.9078 73.7781 18.9292C69.8117 18.9507 68.1211 16.2678 68.1211 10.0007C68.1211 3.73354 69.8117 1.05064 73.7781 1.07209ZM71.1555 10.0007C71.1555 15.4308 71.6757 16.783 73.7781 16.783C75.8589 16.783 76.3791 15.4308 76.3791 10.0007C76.3791 4.54909 75.8589 3.19691 73.7781 3.21847C71.6757 3.23992 71.1555 4.59209 71.1555 10.0007Z"
fill="#FBFBFB"
/>
<path
d="M85.0819 18.5624L82.481 18.5838V1.41351H87.0544L91.3243 15.4073H91.3676V1.41351H93.968V18.5838H89.677L85.1254 3.51689H85.0819V18.5624Z"
fill="#FBFBFB"
/>
<path
d="M100.468 1.41351H97.8677V18.5838H100.468V1.41351Z"
fill="#FBFBFB"
/>
<path
d="M111.139 8.06885H114.174C114.174 3.17534 112.591 1.07196 108.906 1.07196C105.028 1.07196 103.358 3.73341 103.358 9.97909C103.358 16.2462 105.028 18.9291 108.906 18.9291C112.591 18.9291 114.174 16.8901 114.195 12.1468H111.161C111.139 15.8599 110.684 16.7828 108.906 16.7828C106.869 16.7828 106.371 15.4307 106.393 9.97909C106.393 4.54896 106.891 3.19678 108.906 3.21823C110.684 3.21823 111.139 4.18413 111.139 8.06885Z"
fill="#FBFBFB"
/>
</svg>
</SvgIcon>
</Link> </Link>
</Toolbar> <Divider
flexItem
orientation="vertical"
color="#FBFBFB"
sx={{ height: "1.2rem", alignSelf: "center", width: "1px" }}
/>
<Typography fontSize="1.25rem">Testnet Onboarding</Typography>
</Stack>
</AppBar> </AppBar>
); );
}; };

View File

@ -44,12 +44,12 @@ const SelectRoleCard = ({ handleAccept, handleRoleChange }: { handleAccept: () =
<FormControlLabel <FormControlLabel
value={Role.Validator} value={Role.Validator}
control={<Radio />} control={<Radio />}
label="Validator" label="Validator / Service Provider"
/> />
<FormControlLabel <FormControlLabel
value={Role.Participant} value={Role.Participant}
control={<Radio />} control={<Radio />}
label="Participant" label="App Publisher"
/> />
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>

View File

@ -1,34 +1,47 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { Document, Page, pdfjs } from 'react-pdf'; import { Document, Page, pdfjs } from "react-pdf";
import { Typography } from '@mui/material'; 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 (
<> <>
<Typography variant='h4' textAlign="center" gutterBottom> <Typography variant="h4" textAlign="center" gutterBottom>
Terms and Conditions Terms and Conditions
</Typography> </Typography>
<div <div
style={{ style={{
height: height, height: height,
overflowY: 'auto', overflowY: "auto",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
alignItems: 'center' alignItems: "center",
}} }}
> >
<Document <Document
file={process.env.PUBLIC_URL + '/TermsAndConditions.pdf'} file={process.env.PUBLIC_URL + "/TermsAndConditions.pdf"}
onLoadSuccess={onDocumentLoadSuccess} onLoadSuccess={onDocumentLoadSuccess}
> >
{Array.apply(null, Array(numPages)) {Array.apply(null, Array(numPages))
@ -40,7 +53,6 @@ const TermsAndConditionsBox = ({height}: {height: string}) => {
pageNumber={page} pageNumber={page}
renderTextLayer={false} renderTextLayer={false}
renderAnnotationLayer={false} renderAnnotationLayer={false}
width={1070}
/> />
); );
})} })}

View File

@ -16,7 +16,12 @@ const TermsAndConditionsDialog: React.FC<TermsDialogProps> = ({ open, onClose })
<TermsAndConditionsBox height='65vh' /> <TermsAndConditionsBox height='65vh' />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} color="primary"> <Button>
<a href="/TermsAndConditions.pdf" download style={{textDecoration: "none", color: "inherit"}}>
Download PDF
</a>
</Button>
<Button onClick={onClose}>
Close Close
</Button> </Button>
</DialogActions> </DialogActions>

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

@ -22,6 +22,7 @@ assert(PROJECT_ID, "Wallet connect project id not provided");
interface ContextValue { interface ContextValue {
connect: () => Promise<void>; connect: () => Promise<void>;
session: SessionTypes.Struct | null; session: SessionTypes.Struct | null;
isSessionLoading: boolean;
signClient: SignClient | undefined; signClient: SignClient | undefined;
checkPersistedState: (client: SignClient) => Promise<void>; checkPersistedState: (client: SignClient) => Promise<void>;
disconnect: () => Promise<void>; disconnect: () => Promise<void>;
@ -30,6 +31,7 @@ interface ContextValue {
const walletConnectContext = createContext<ContextValue>({ const walletConnectContext = createContext<ContextValue>({
connect: () => Promise.resolve(), connect: () => Promise.resolve(),
session: null, session: null,
isSessionLoading: true,
signClient: undefined, signClient: undefined,
checkPersistedState: () => Promise.resolve(), checkPersistedState: () => Promise.resolve(),
disconnect: () => Promise.resolve(), disconnect: () => Promise.resolve(),
@ -47,6 +49,7 @@ export const WalletConnectProvider = ({
}) => { }) => {
const [signClient, setSignClient] = useState<SignClient>(); const [signClient, setSignClient] = useState<SignClient>();
const [session, setSession] = useState<SessionTypes.Struct | null>(null); const [session, setSession] = useState<SessionTypes.Struct | null>(null);
const [isSessionLoading, setIsSessionLoading] = useState(true);
const isSignClientInitializing = useRef<boolean>(false); const isSignClientInitializing = useRef<boolean>(false);
const navigate = useNavigate(); const navigate = useNavigate();
@ -59,6 +62,7 @@ export const WalletConnectProvider = ({
} }
setSession(null); setSession(null);
setIsSessionLoading(false);
}, [signClient, session]); }, [signClient, session]);
const checkPersistedState = useCallback(async (client: SignClient) => { const checkPersistedState = useCallback(async (client: SignClient) => {
@ -67,6 +71,8 @@ export const WalletConnectProvider = ({
const session = client.session.get(client.session.keys[lastKeyIndex]); const session = client.session.get(client.session.keys[lastKeyIndex]);
setSession(session); setSession(session);
} }
setIsSessionLoading(false);
}, []); }, []);
const subscribeToEvents = useCallback( const subscribeToEvents = useCallback(
@ -76,10 +82,12 @@ export const WalletConnectProvider = ({
const currentSession = client.session.get(topic); const currentSession = client.session.get(topic);
const updatedSession = { ...currentSession, namespaces }; const updatedSession = { ...currentSession, namespaces };
setSession(updatedSession); setSession(updatedSession);
setIsSessionLoading(false);
}); });
client.on("session_delete", () => { client.on("session_delete", () => {
setSession(null); setSession(null);
setIsSessionLoading(false);
navigate("/"); navigate("/");
}); });
}, },
@ -122,6 +130,13 @@ export const WalletConnectProvider = ({
chains: [`cosmos:${process.env.REACT_APP_LACONICD_CHAIN_ID}`], chains: [`cosmos:${process.env.REACT_APP_LACONICD_CHAIN_ID}`],
events: [], events: [],
}, },
laconic: {
methods: [
"cosmos_sendTransaction",
],
chains: [`laconic:${process.env.REACT_APP_LACONICD_CHAIN_ID}`],
events: [],
},
}; };
const { uri, approval } = await signClient.connect({ const { uri, approval } = await signClient.connect({
@ -133,12 +148,14 @@ export const WalletConnectProvider = ({
try { try {
const session = await approval(); const session = await approval();
setSession(session); setSession(session);
setIsSessionLoading(false);
} catch (error) { } catch (error) {
enqueueSnackbar("User rejected pairing request", { variant: "error" }); enqueueSnackbar("User rejected pairing request", { variant: "error" });
console.log(error); console.log(error);
} }
web3Modal.closeModal(); web3Modal.closeModal();
} }
}; };
useEffect(() => { useEffect(() => {
@ -152,6 +169,7 @@ export const WalletConnectProvider = ({
value={{ value={{
connect, connect,
session, session,
isSessionLoading,
signClient, signClient,
checkPersistedState, checkPersistedState,
disconnect, disconnect,
@ -163,12 +181,13 @@ export const WalletConnectProvider = ({
}; };
export const useWalletConnectContext = () => { export const useWalletConnectContext = () => {
const { connect, session, signClient, checkPersistedState, disconnect } = const { connect, session, signClient, isSessionLoading, checkPersistedState, disconnect } =
useContext(walletConnectContext); useContext(walletConnectContext);
return { return {
connect, connect,
session, session,
isSessionLoading,
signClient, signClient,
checkPersistedState, checkPersistedState,
disconnect, disconnect,

34
src/layout/Layout.tsx Normal file
View File

@ -0,0 +1,34 @@
import { Button, Typography } from "@mui/material";
import React, { PropsWithChildren } from "react";
import { Container } from "../components/Container";
import { ArrowBack } from "@mui/icons-material";
import { useNavigate } from "react-router-dom";
export const Layout: React.FC<
PropsWithChildren<{
title: string;
backLinkTitle?: string;
noBackButton?: boolean;
}>
> = ({ children, title, backLinkTitle = "Home", noBackButton = false }) => {
const navigate = useNavigate();
return (
<Container boxProps={{ sx: { backgroundColor: "inherit", padding: 0 } }}>
{noBackButton ? null : (
<Button
startIcon={<ArrowBack />}
color="info"
sx={{ mb: 4 }}
onClick={() => navigate("/")}
>
{backLinkTitle}
</Button>
)}
<Typography variant="h4" sx={{ mb: 4 }}>
{title}
</Typography>
<Container>{children}</Container>
</Container>
);
};

View File

@ -1,44 +1,38 @@
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 { Avatar, Button, Stack, Typography } from "@mui/material";
Toolbar,
Avatar,
Button,
Typography,
Container
} from "@mui/material";
import { useWalletConnectContext } from "../context/WalletConnectContext"; import { useWalletConnectContext } from "../context/WalletConnectContext";
import { Container } from "../components/Container";
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 (
<> <Stack justifyContent="center" alignItems="center">
<Toolbar variant="dense"> <Container
boxProps={{
<Button sx: {
variant="outlined" display: "flex",
style={{ flexDirection: "row",
marginLeft: "auto", justifyContent: "space-between",
mb: 4,
},
}} }}
color="error"
onClick={disconnectHandler}
> >
Disconnect
</Button>
</Toolbar>
<Container maxWidth="md">
{session && ( {session && (
<div style={{ display: "flex", flexDirection: "column" }}> <Stack spacing={0.5}>
<div <div
style={{ style={{
display: "flex", display: "flex",
@ -47,7 +41,7 @@ const SignPageLayout = () => {
}} }}
> >
<Typography variant="body2"> <Typography variant="body2">
Connected to: <b> {session.peer.metadata.name}</b>{" "} <b>Connected to:</b> {session.peer.metadata.name}
</Typography> </Typography>
<Avatar <Avatar
variant="square" variant="square"
@ -62,13 +56,21 @@ const SignPageLayout = () => {
/> />
</div> </div>
<Typography variant="body2"> <Typography variant="body2">
Session ID: <b>{session.topic} </b> <b>Session ID:</b> {session.topic}
</Typography> </Typography>
</div> </Stack>
)} )}
<Outlet /> <Button
variant="outlined"
color="error"
onClick={disconnectHandler}
sx={{ color: "text.primary" }}
>
Disconnect
</Button>
</Container> </Container>
</> <Outlet />
</Stack>
); );
}; };

View File

@ -1,62 +1,57 @@
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 } from "@mui/material";
import { useWalletConnectContext } from "../context/WalletConnectContext"; import { useWalletConnectContext } from "../context/WalletConnectContext";
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();
}; };
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg" sx={{ height: "75%" }}>
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
marginTop={10}
sx={{
border: 1,
borderColor: 'grey.500',
}}
padding={5}
>
<Typography variant="h5" component="h1" gutterBottom color={colors.red[400]}>
Disclaimer
</Typography>
<Typography variant="body1">
{WALLET_DISCLAIMER_MSG}
</Typography>
</Box>
<Box <Box
display="flex" display="flex"
flexDirection="column" flexDirection="column"
alignItems="center" alignItems="center"
padding={5} padding={5}
justifyContent="center" justifyContent="center"
height="100%"
> >
<Button <Button variant="contained" onClick={handler}>
variant="contained"
onClick={handler}
>
Connect Wallet Connect Wallet
</Button> </Button>
</Box> </Box>

View File

@ -1,19 +1,22 @@
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";
import TermsAndConditionsBox from '../components/TermsAndConditionsBox'; import TermsAndConditionsBox from "../components/TermsAndConditionsBox";
import { Container } from "../components/Container";
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");
}; };
return ( return (
<> <Container>
<Box <Box
display="flex" display="flex"
flexDirection="column" flexDirection="column"
@ -22,25 +25,61 @@ const LandingPage = () => {
margin={10} margin={10}
sx={{ sx={{
border: 1, border: 1,
borderColor: 'grey.500', borderColor: "grey.500",
borderRadius: 1,
}} }}
padding={5} padding={5}
> >
<Typography variant="h6"> <Typography variant="h6">
Welcome to the LORO Testnet Onboarding App. The detailed instructions for completing this first step are found in the{' '} Welcome to the LORO Testnet Onboarding App. The detailed instructions
<a href="https://github.com/hyphacoop/loro-testnet/" target="_blank" rel="noopener noreferrer">LORO testnet repo</a>. for completing this first step are found in the{" "}
Once your onboarding transaction has been submitted, await the completion of stage0. The genesis.json file and peer nodes will then be <a
published in the aforementioned repository for validators to begin stage1. Once enough validators are online and the Laconic chain is running, href="https://github.com/hyphacoop/loro-testnet/"
those same validators can complete their service provider setup. Once service providers are live, app publishers can start deploying webapps to individual service providers. target="_blank"
rel="noopener noreferrer"
>
LORO testnet repo
</a>
. Once your onboarding transaction has been submitted, await the
completion of stage0. The genesis.json file and peer nodes will then
be published in the aforementioned repository for validators to begin
stage1. Once enough validators are online and the Laconic chain is
running, 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
<Box mt={2} display="flex" justifyContent="center"> height="43vh"
<Button variant="contained" color="primary" onClick={handleAccept}> onLoad={() => {
setIsDisabled(false);
}}
/>
<Box m={2} display="flex" justifyContent="center" gap={2}>
<Button
variant="outlined"
color="primary"
sx={{ color: "text.primary" }}
disabled={isDisabled}
>
<a
href="/TermsAndConditions.pdf"
download
style={{textDecoration: "none", color: "inherit"}}
>
Download PDF
</a>
</Button>
<Button
variant="contained"
color="primary"
onClick={handleAccept}
disabled={isDisabled}
>
Accept Accept
</Button> </Button>
</Box> </Box>
</> </Container>
); );
}; };

View File

@ -7,48 +7,49 @@ import { Registry } from "@cerc-io/registry-sdk";
import SumsubWebSdk from "@sumsub/websdk-react"; 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 {
import { ENABLE_KYC, SUBSCRIBER_ID_HASH_KEY } from "../constants"; config,
fetchAccessToken,
getAccessTokenExpirationHandler,
options,
} from "../utils/sumsub";
import { ENABLE_KYC, HASHED_SUBSCRIBER_ID_KEY } from "../constants";
import { Participant } from "../types";
import { CodeBlock } from "../components/CodeBlock";
interface Participant { const registry = new Registry(process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!);
cosmosAddress: string;
nitroAddress: string;
role: string;
kycId: string;
}
const registry = new Registry(
process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!
);
const OnboardingSuccess = () => { const OnboardingSuccess = () => {
const location = useLocation(); const location = useLocation();
const { cosmosAddress } = location.state as { const { laconicAddress } = location.state as {
cosmosAddress?: string laconicAddress?: string;
}; };
const [participant, setParticipant] = useState<Participant>(); const [participant, setParticipant] = useState<Participant>();
const [token, setToken] = useState<string>(''); const [token, setToken] = useState<string>("");
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const messageHandler: MessageHandler = (event, payload) => { const messageHandler: MessageHandler = (event, payload) => {
console.log('sumsubEvent:', event, payload); console.log("sumsubEvent:", event, payload);
}; };
useEffect(() => { useEffect(() => {
const fetchParticipants = async () => { const fetchParticipants = async () => {
try { try {
if (!cosmosAddress) { if (!laconicAddress) {
enqueueSnackbar("Cosmos address is not provided", { variant: "error" }); enqueueSnackbar("Laconic address is not provided", {
variant: "error",
});
return; return;
} }
const participant: Participant = await registry.getParticipantByAddress(cosmosAddress); const participant: Participant =
await registry.getParticipantByAddress(laconicAddress);
if (!participant) { if (!participant) {
enqueueSnackbar("Participant not found", { variant: "error" }); enqueueSnackbar("Participant not found", { variant: "error" });
return; return;
} }
localStorage.removeItem(SUBSCRIBER_ID_HASH_KEY); localStorage.removeItem(HASHED_SUBSCRIBER_ID_KEY);
setParticipant(participant); setParticipant(participant);
} catch (error) { } catch (error) {
@ -57,7 +58,7 @@ const OnboardingSuccess = () => {
}; };
fetchParticipants(); fetchParticipants();
}, [cosmosAddress]); }, [laconicAddress]);
useEffect(() => { useEffect(() => {
const getToken = async (userId: string) => { const getToken = async (userId: string) => {
@ -66,13 +67,13 @@ const OnboardingSuccess = () => {
setLoading(false); setLoading(false);
}; };
if (cosmosAddress && ENABLE_KYC) { if (laconicAddress && ENABLE_KYC) {
getToken(cosmosAddress).catch(error => { getToken(laconicAddress).catch((error) => {
console.error(error); console.error(error);
alert("Failed to fetch token"); alert("Failed to fetch token");
}); });
} }
}, [cosmosAddress]); }, [laconicAddress]);
return ( return (
<> <>
@ -88,41 +89,35 @@ const OnboardingSuccess = () => {
<Typography variant="body1"> <Typography variant="body1">
Participant onboarded: <br /> Participant onboarded: <br />
</Typography> </Typography>
<Box <CodeBlock>
sx={{
backgroundColor: "lightgray",
padding: 3,
wordWrap: "break-word",
marginBottom: 6,
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{participant && ( {participant && (
<div> <div>
Cosmos Address: {participant.cosmosAddress} <br /> Laconic Address: {participant.laconicAddress} <br />
Nitro Address: {participant.nitroAddress} <br /> Nitro Address: {participant.nitroAddress} <br />
Role: {participant.role} <br /> Role: {participant.role} <br />
KYC ID: {participant.kycId} <br /> KYC ID: {participant.kycId} <br />
<br /> <br />
</div> </div>
)} )}
</pre> </CodeBlock>
</Box>
{ENABLE_KYC ? ( {ENABLE_KYC ? (
<Box> <Box>
<Typography variant="h5">KYC Status</Typography> <Typography variant="h5">KYC Status</Typography>
{!loading && token && cosmosAddress && ( {!loading && token && laconicAddress && (
<SumsubWebSdk <SumsubWebSdk
accessToken={token} accessToken={token}
expirationHandler={getAccessTokenExpirationHandler(cosmosAddress)} expirationHandler={getAccessTokenExpirationHandler(
laconicAddress,
)}
config={config} config={config}
options={options} options={options}
onMessage={messageHandler} onMessage={messageHandler}
/> />
)} )}
</Box> </Box>
) : '' ) : (
} ""
)}
</Box> </Box>
<Typography variant="h5">Next Steps</Typography> <Typography variant="h5">Next Steps</Typography>
<Box <Box
@ -133,18 +128,35 @@ const OnboardingSuccess = () => {
marginTop={3} marginTop={3}
sx={{ sx={{
border: 1, border: 1,
borderColor: 'grey.500', borderColor: "grey.500",
}} }}
padding={5} padding={5}
> >
<Typography variant="body1" gutterBottom sx={{ p: 2 }}> <Typography variant="body1" gutterBottom sx={{ p: 2 }}>
For participants, await the start of the stage 1 chain, which will be announced in various social media channels. In the meantime, familiarize yourself with the{' '} For app publishers, await the start of the stage 1 chain, which will
<a href="https://github.com/hyphacoop/loro-testnet/blob/main/docs/publishing-webapps.md" target="_blank" rel="noopener noreferrer">webapp publishing workflow</a>{' '} be announced in various social media channels. In the meantime,
as this is the main task you will be participating in.<br /> familiarize yourself with the{" "}
<a
href="https://github.com/hyphacoop/loro-testnet/blob/main/docs/publishing-webapps.md"
target="_blank"
rel="noopener noreferrer"
>
webapp publishing workflow
</a>{" "}
as this is the main task you will be participating in.
<br /> <br />
For validators, ensure your service provider is running and ready to deploy webapps. Await publication of the laconicd version, genesis file, and peers to the LORO testnet repo, then follow{' '} <br />
<a href="https://git.vdb.to/cerc-io/testnet-laconicd-stack/src/branch/main/testnet-onboarding-validator.md#join-as-a-validator-on-stage1" target="_blank" rel="noopener noreferrer">these instructions</a>{' '} For validators, ensure your service provider is running and ready to
for joining stage 1 as a validator. deploy webapps. Await publication of the laconicd version, genesis
file, and peer info to the LORO testnet repo, then follow{" "}
<a
href="https://git.vdb.to/cerc-io/testnet-laconicd-stack/src/branch/main/testnet-onboarding-validator.md#join-as-a-validator-on-stage1"
target="_blank"
rel="noopener noreferrer"
>
these instructions
</a>{" "}
for joining stage 1 as a validator/service provider.
</Typography> </Typography>
</Box> </Box>
</> </>

View File

@ -1,213 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
import { Box, Card, CardContent, Grid, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton/LoadingButton";
import {
MsgOnboardParticipantEncodeObject,
typeUrlMsgOnboardParticipant,
} from "@cerc-io/registry-sdk";
import { StargateClient } from "@cosmjs/stargate";
import { useWalletConnectContext } from "../context/WalletConnectContext";
import SelectRoleCard, {Role} from "../components/SelectRoleCard";
import { SUBSCRIBER_ID_HASH_KEY } from "../constants";
const SignWithCosmos = () => {
const { session, signClient } = useWalletConnectContext();
const location = useLocation();
const [isLoading, setIsLoading] = useState(false);
const [balance, setBalance] = useState('');
const [isRequesting, setIsRequesting] = useState(false);
const [isTncAccepted, setIsTncAccepted] = useState(false);
const [role, setRole] = useState(Role.Participant);
const navigate = useNavigate();
const {message: innerMessage, cosmosAddress, receivedEthSig: ethSignature} = location.state as {
message?: {
msg: string;
address: string;
};
cosmosAddress?: string;
receivedEthSig?: string;
};
const ethAddress = innerMessage!.address;
const subscriberIdHash = localStorage.getItem(SUBSCRIBER_ID_HASH_KEY);
const createCosmosClient = useCallback(async (endpoint: string) => {
return await StargateClient.connect(endpoint);
}, []);
const onboardParticipantMsg: MsgOnboardParticipantEncodeObject =
useMemo(() => {
return {
typeUrl: typeUrlMsgOnboardParticipant,
value: {
participant: cosmosAddress!,
ethPayload: innerMessage,
ethSignature: ethSignature!,
kycId: subscriberIdHash!,
role
},
};
}, [cosmosAddress, innerMessage, ethSignature, subscriberIdHash, role]);
const handleTokenRequest = async () => {
try {
setIsRequesting(true);
const response = await fetch(`${process.env.REACT_APP_FAUCET_ENDPOINT!}/faucet`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
address: cosmosAddress,
}),
});
if (response.ok) {
enqueueSnackbar('Tokens sent successfully', { variant: "success" });
} else {
const errorResponse = await response.json();
if (response.status === 429) {
enqueueSnackbar(`${response.statusText} : ${errorResponse.error}`, { variant: "error" });
} else {
throw new Error(errorResponse.error);
}
}
getBalances();
} catch (error) {
console.error(error);
enqueueSnackbar("Error getting tokens from faucet", { variant: "error" });
} finally {
setIsRequesting(false);
}
};
const sendTransaction = async (
transactionMessage: MsgOnboardParticipantEncodeObject
) => {
if (!ethAddress) {
enqueueSnackbar("Set nitro address");
return;
}
try {
setIsLoading(true);
const params = { transactionMessage, signer: cosmosAddress };
const responseFromWallet = await signClient!.request<{
code: number;
}>({
topic: session!.topic,
chainId: `cosmos:${process.env.REACT_APP_LACONICD_CHAIN_ID}`,
request: {
method: "cosmos_sendTransaction",
params,
},
});
if (responseFromWallet.code !== 0) {
enqueueSnackbar("Transaction not sent", { variant: "error" });
} else {
navigate("/onboarding-success", {
state: {
cosmosAddress
}
});
}
} catch (error) {
console.error(error);
enqueueSnackbar("Error in sending transaction", { variant: "error" });
} finally {
setIsLoading(false);
}
};
const getBalances = useCallback(async () => {
try {
const cosmosClient = await createCosmosClient(process.env.REACT_APP_LACONICD_RPC_ENDPOINT!);
const balance = await cosmosClient.getBalance(cosmosAddress!, process.env.REACT_APP_LACONICD_DENOM!);
setBalance(balance.amount);
} catch (error) {
console.error('Error fetching balance:', error);
throw error;
}
}, [cosmosAddress, createCosmosClient]);
useEffect(() => {
getBalances();
}, [getBalances]);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
marginTop: 6,
gap: 1,
}}
>
<Typography variant="h5" display={`${isTncAccepted ? "none" : "block"}`}>Please accept terms and conditions to continue</Typography>
<SelectRoleCard handleAccept={() => setIsTncAccepted(true)} handleRoleChange={setRole}/>
<Typography variant="h5">Send transaction to chain</Typography>
<Typography>Cosmos Account:</Typography>
<Card className='mt-1 mb-1'>
<CardContent>
<Grid container spacing={2}>
<Grid item xs={9}>
<Typography variant="body1">Address: {cosmosAddress}</Typography>
<Typography variant="body1">Balance: {balance} {process.env.REACT_APP_LACONICD_DENOM}</Typography>
</Grid>
<Grid item xs={3} container justifyContent="flex-end">
<LoadingButton
variant="contained"
onClick={handleTokenRequest}
disabled={isTncAccepted ? isRequesting : !isTncAccepted}
loading={isRequesting}
>
Request tokens from Faucet
</LoadingButton>
</Grid>
</Grid>
</CardContent>
</Card>
<Typography variant="body1">
Onboarding message: <br />
</Typography>
<Box
sx={{
backgroundColor: "lightgray",
padding: 3,
wordWrap: "break-word",
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{JSON.stringify(onboardParticipantMsg, null, 2)}{" "}
</pre>
</Box>
<Box
sx={{
paddingBottom: 2,
}}>
<LoadingButton
variant="contained"
onClick={async () => {
await sendTransaction(onboardParticipantMsg);
}}
loading={isLoading}
disabled={isTncAccepted ? (balance === '0') : !isTncAccepted}
>
Send transaction
</LoadingButton>
</Box>
</Box>
);
};
export default SignWithCosmos;

View File

@ -0,0 +1,222 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
import { Box, Divider, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton/LoadingButton";
import {
MsgOnboardParticipantEncodeObject,
typeUrlMsgOnboardParticipant,
} from "@cerc-io/registry-sdk";
import { StargateClient } from "@cosmjs/stargate";
import { useWalletConnectContext } from "../context/WalletConnectContext";
import SelectRoleCard, { Role } from "../components/SelectRoleCard";
import { HASHED_SUBSCRIBER_ID_KEY } from "../constants";
import { Layout } from "../layout/Layout";
import { CodeBlock } from "../components/CodeBlock";
const SignWithLaconic = () => {
const { session, signClient } = useWalletConnectContext();
const location = useLocation();
const [isLoading, setIsLoading] = useState(false);
const [balance, setBalance] = useState("");
const [isRequesting, setIsRequesting] = useState(false);
const [isTncAccepted, setIsTncAccepted] = useState(false);
const [role, setRole] = useState(Role.Participant);
const navigate = useNavigate();
const {
message: innerMessage,
laconicAddress,
receivedEthSig: ethSignature,
} = location.state as {
message?: {
msg: string;
address: string;
};
laconicAddress?: string;
receivedEthSig?: string;
};
const ethAddress = innerMessage!.address;
const subscriberIdHash = localStorage.getItem(HASHED_SUBSCRIBER_ID_KEY);
const createLaconicClient = useCallback(async (endpoint: string) => {
return await StargateClient.connect(endpoint);
}, []);
const onboardParticipantMsg: MsgOnboardParticipantEncodeObject =
useMemo(() => {
return {
typeUrl: typeUrlMsgOnboardParticipant,
value: {
participant: laconicAddress!,
ethPayload: innerMessage,
ethSignature: ethSignature!,
kycId: subscriberIdHash!,
role,
},
};
}, [laconicAddress, innerMessage, ethSignature, subscriberIdHash, role]);
const handleTokenRequest = async () => {
try {
setIsRequesting(true);
const response = await fetch(
`${process.env.REACT_APP_FAUCET_ENDPOINT!}/faucet`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
address: laconicAddress,
}),
},
);
if (response.ok) {
enqueueSnackbar("Tokens sent successfully", { variant: "success" });
} else {
const errorResponse = await response.json();
if (response.status === 429) {
enqueueSnackbar(`${response.statusText} : ${errorResponse.error}`, {
variant: "error",
});
} else {
throw new Error(errorResponse.error);
}
}
getBalances();
} catch (error) {
console.error(error);
enqueueSnackbar("Error getting tokens from faucet", { variant: "error" });
} finally {
setIsRequesting(false);
}
};
const sendTransaction = async (
transactionMessage: MsgOnboardParticipantEncodeObject,
) => {
if (!ethAddress) {
enqueueSnackbar("Set nitro address");
return;
}
try {
setIsLoading(true);
enqueueSnackbar("View and sign the message from your Laconic Wallet", {
variant: "info",
});
const params = { transactionMessage, signer: laconicAddress };
const responseFromWallet = await signClient!.request<{
code: number;
}>({
topic: session!.topic,
chainId: `laconic:${process.env.REACT_APP_LACONICD_CHAIN_ID}`,
request: {
method: "cosmos_sendTransaction",
params,
},
});
if (responseFromWallet.code !== 0) {
enqueueSnackbar("Transaction not sent", { variant: "error" });
} else {
navigate("/onboarding-success", {
state: {
laconicAddress,
},
});
}
} catch (error) {
console.error(error);
enqueueSnackbar("Error in sending transaction", { variant: "error" });
} finally {
setIsLoading(false);
}
};
const getBalances = useCallback(async () => {
try {
const laconicClient = await createLaconicClient(
process.env.REACT_APP_LACONICD_RPC_ENDPOINT!,
);
const balance = await laconicClient.getBalance(
laconicAddress!,
process.env.REACT_APP_LACONICD_DENOM!,
);
setBalance(balance.amount);
} catch (error) {
console.error("Error fetching balance:", error);
throw error;
}
}, [laconicAddress, createLaconicClient]);
useEffect(() => {
getBalances();
}, [getBalances]);
return (
<>
{!isTncAccepted && (
<Layout
title="Please accept the terms and conditions to continue"
noBackButton
>
<SelectRoleCard
handleAccept={() => setIsTncAccepted(true)}
handleRoleChange={setRole}
/>
</Layout>
)}
<Layout title="Send transaction to chain" noBackButton>
<Typography>Laconic Account:</Typography>
<Box sx={{ backgroundColor: "#29292E", p: 2, borderRadius: 1, mb: 2 }}>
<Typography variant="body1">Address: {laconicAddress}</Typography>
<Typography variant="body1">
Balance: {balance} {process.env.REACT_APP_LACONICD_DENOM}
</Typography>
</Box>
<LoadingButton
variant="contained"
onClick={handleTokenRequest}
disabled={isTncAccepted ? isRequesting : !isTncAccepted}
loading={isRequesting}
>
Request tokens from Faucet
</LoadingButton>
<Divider flexItem sx={{ my: 2 }} />
<Typography variant="body1">Onboarding message:</Typography>
<CodeBlock>{JSON.stringify(onboardParticipantMsg, null, 2)} </CodeBlock>
<Box
sx={{
paddingBottom: 2,
mt: 2,
}}
>
<LoadingButton
variant="contained"
onClick={async () => {
await sendTransaction(onboardParticipantMsg);
}}
loading={isLoading}
disabled={isTncAccepted ? balance === "0" : !isTncAccepted}
>
Send transaction
</LoadingButton>
</Box>
</Layout>
</>
);
};
export default SignWithLaconic;

View File

@ -1,44 +1,53 @@
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";
import { import { Select, MenuItem, Box, Typography, Stack } from "@mui/material";
Select, import LoadingButton from "@mui/lab/LoadingButton";
MenuItem,
Box,
Typography,
} from "@mui/material";
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";
import { Layout } from "../layout/Layout";
import { CodeBlock } from "../components/CodeBlock";
const SignWithNitroKey = () => { const SignWithNitroKey = () => {
const { session, signClient, isSessionLoading } = useWalletConnectContext();
const { session, signClient, checkPersistedState } =
useWalletConnectContext();
useEffect(() => {
if (signClient && !session) {
checkPersistedState(signClient);
}
}, [session, signClient, checkPersistedState]);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
if (isSessionLoading) {
return;
}
if (!session) {
navigate("/connect-wallet?redirectTo=sign-with-nitro-key", {
state: location.state,
});
}
}, [session, isSessionLoading, navigate, location.state]);
const [ethAddress, setEthAddress] = useState(""); const [ethAddress, setEthAddress] = useState("");
const [ethSignature, setEthSignature] = useState(""); const [ethSignature, setEthSignature] = useState("");
const [cosmosAddress, setCosmosAddress] = useState(""); const [laconicAddress, setLaconicAddress] = 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) {
setIsLoading(false); setIsLoading(false);
enqueueSnackbar("Subscriber ID not found. Please verify your email and try again", { variant: "error" }); enqueueSnackbar(
"Subscriber ID not found. Please verify your email and try again",
{ variant: "error" },
);
} }
}, [subscriberIdHash]); }, [subscriberIdHash]);
@ -53,6 +62,11 @@ 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({
@ -70,15 +84,15 @@ const SignWithNitroKey = () => {
navigate("/user-verification", { navigate("/user-verification", {
state: { state: {
message, message,
cosmosAddress, laconicAddress,
receivedEthSig, receivedEthSig,
}, },
}); });
} else { } else {
navigate("/sign-with-cosmos", { navigate("/sign-with-laconic", {
state: { state: {
message, message,
cosmosAddress, laconicAddress,
receivedEthSig, receivedEthSig,
}, },
}); });
@ -92,24 +106,16 @@ const SignWithNitroKey = () => {
}; };
return ( return (
<div> <Layout title="New Session" noBackButton>
{session ? ( {session ? (
<Box <Stack spacing={2}>
sx={{
display: "flex",
flexDirection: "column",
marginTop: 6,
gap: 1,
}}
>
<Typography variant="h5">Sign with Nitro key</Typography>
<Typography variant="body1">Select Laconic account:</Typography> <Typography variant="body1">Select Laconic account:</Typography>
<Select <Select
labelId="demo-simple-select-label" labelId="demo-simple-select-label"
id="demo-simple-select" id="demo-simple-select"
value={cosmosAddress} value={laconicAddress}
onChange={(e: any) => { onChange={(e: any) => {
setCosmosAddress(e.target.value); setLaconicAddress(e.target.value);
}} }}
style={{ maxWidth: "600px", display: "block" }} style={{ maxWidth: "600px", display: "block" }}
> >
@ -136,15 +142,9 @@ const SignWithNitroKey = () => {
))} ))}
</Select> </Select>
{(Boolean(ethAddress) && Boolean(cosmosAddress)) && (<Box {Boolean(ethAddress) && Boolean(laconicAddress) && (
sx={{ <CodeBlock>{canonicalStringify(message, null, 2)} </CodeBlock>
backgroundColor: "lightgray", )}
padding: 3,
wordWrap: "break-word",
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>{canonicalStringify(message, null, 2)} </pre>
</Box>)}
<Box> <Box>
<LoadingButton <LoadingButton
variant="contained" variant="contained"
@ -156,11 +156,11 @@ const SignWithNitroKey = () => {
Sign using Nitro key Sign using Nitro key
</LoadingButton> </LoadingButton>
</Box> </Box>
</Box> </Stack>
) : ( ) : (
<>Loading...</> <>Loading...</>
)} )}
</div> </Layout>
); );
}; };

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));
} }

View File

@ -18,13 +18,13 @@ const UserVerification = () => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const {message, cosmosAddress, receivedEthSig} = location.state as { const {message, laconicAddress, receivedEthSig} = location.state as {
message?: string; message?: string;
cosmosAddress?: string; laconicAddress?: string;
receivedEthSig?: string; receivedEthSig?: string;
}; };
const userId = cosmosAddress; const userId = laconicAddress;
useEffect(() => { useEffect(() => {
const getToken = async (userId: string) => { const getToken = async (userId: string) => {
@ -46,15 +46,15 @@ const UserVerification = () => {
if (applicationSubmitted && kycId !== '') { if (applicationSubmitted && kycId !== '') {
const kycIdHash = ethers.utils.sha256(ethers.utils.toUtf8Bytes(kycId)); const kycIdHash = ethers.utils.sha256(ethers.utils.toUtf8Bytes(kycId));
navigate("/sign-with-cosmos", { navigate("/sign-with-laconic", {
state: { state: {
message, message,
cosmosAddress, laconicAddress,
receivedEthSig, receivedEthSig,
kycIdHash kycIdHash
}}); }});
} }
}, [applicationSubmitted, kycId, navigate, cosmosAddress, message, receivedEthSig]); }, [applicationSubmitted, kycId, navigate, laconicAddress, message, receivedEthSig]);
const messageHandler: MessageHandler = (event, payload) => { const messageHandler: MessageHandler = (event, payload) => {
console.log('sumsubEvent:', event, payload); console.log('sumsubEvent:', event, payload);

279
src/pages/Validator.tsx Normal file
View File

@ -0,0 +1,279 @@
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";
import { Layout } from "../layout/Layout";
import { CodeBlock } from "../components/CodeBlock";
const Validator = () => {
const { session, signClient, isSessionLoading } = useWalletConnectContext();
const navigate = useNavigate();
const [laconicAddress, setLaconicAddress] = 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 (isSessionLoading) {
return;
}
if (!session) {
navigate("/connect-wallet?redirectTo=validator");
}
}, [session, navigate, isSessionLoading]);
useEffect(() => {
if (!laconicAddress) {
setParticipant(null);
return;
}
const fetchParticipant = async () => {
const registry = new Registry(
process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!,
);
try {
const fetchedParticipant =
await registry.getParticipantByAddress(laconicAddress);
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();
}, [laconicAddress]);
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:
laconicAddress &&
toBech32("laconicvaloper", fromBech32(laconicAddress).data),
pubkey: encodedPubKey,
value: {
amount: process.env.REACT_APP_STAKING_AMOUNT!,
denom: process.env.REACT_APP_LACONICD_DENOM!,
},
};
}, [laconicAddress, 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: laconicAddress,
};
const response = await signClient!.request<{ code: number }>({
topic: session!.topic,
chainId: `laconic:${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 (
<Layout title="Create a validator">
<Typography variant="body1">Select Laconic account:</Typography>
<Select
sx={{ marginBottom: 2 }}
id="laconic-address-select"
value={laconicAddress}
onChange={(e) => setLaconicAddress(e.target.value)}
style={{ maxWidth: "600px", display: "block" }}
>
{session?.namespaces.laconic.accounts.map((address, index) => (
<MenuItem value={address.split(":")[2]} key={index}>
{address.split(":")[2]}
</MenuItem>
))}
</Select>
{Boolean(laconicAddress) && (
<>
{participant ? (
<Typography>Onboarded participant</Typography>
) : (
<Typography>No participant found</Typography>
)}
{participant && (
<CodeBlock>
Laconic Address: {participant.laconicAddress} <br />
Nitro Address: {participant.nitroAddress} <br />
Role: {participant.role} <br />
KYC ID: {participant.kycId} <br />
</CodeBlock>
)}
{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>
<CodeBlock>
{`laconic-so deployment --dir testnet-laconicd-deployment exec laconicd "laconicd cometbft show-validator" | jq -r .key`}
</CodeBlock>
<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>
<CodeBlock>
{JSON.stringify(msgCreateValidator, replacer, 2)}
</CodeBlock>
<Box marginTop={1} marginBottom={1}>
<LoadingButton
variant="contained"
onClick={sendTransaction}
loading={isLoading}
disabled={isError}
>
Send transaction
</LoadingButton>
</Box>
</>
)}
</>
)}
</Layout>
);
};
export default Validator;

View File

@ -0,0 +1,35 @@
import React from "react";
import { useLocation } from "react-router-dom";
import { Link, Typography } from "@mui/material";
import { Layout } from "../layout/Layout";
import { CodeBlock } from "../components/CodeBlock";
const ValidatorSuccess = () => {
const location = useLocation();
const { validatorAddress } = location.state as {
validatorAddress?: string;
};
return (
<Layout title="Validator created successfully">
<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>
<CodeBlock>
{`laconic-so deployment --dir testnet-laconicd-deployment exec laconicd "laconicd query staking validators --output json" | jq '.validators[] | select(.operator_address == "${validatorAddress}")'`}
</CodeBlock>
</Layout>
);
};
export default ValidatorSuccess;

6
src/types.ts Normal file
View File

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

3360
yarn.lock

File diff suppressed because it is too large Load Diff