Add support for onboarding to laconicd (#13)

* Change logo and app name

* Get cosmos address from route

* Navigate to second page on receiving eth signature

* Add title to page

* Use consistent formatting

* Handle review changes

* Remove unecessary field from onboarding message
This commit is contained in:
Adwait Gharpure 2024-07-05 15:12:57 +05:30 committed by GitHub
parent 60c95659a0
commit 359eddd385
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 118 additions and 213 deletions

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import ConnectWallet from "./pages/ConnectWallet"; import ConnectWallet from "./pages/ConnectWallet";
import SignWithEthereum from "./pages/SignWithEthereum"; import SignWithEthereum from "./pages/SignWithEthereum";
@ -14,13 +14,16 @@ function App() {
<WalletConnectProvider> <WalletConnectProvider>
<Routes> <Routes>
<Route path="/" element={<ConnectWallet />} /> <Route path="/" element={<ConnectWallet />} />
<Route element={<SignPageLayout />} > <Route element={<SignPageLayout />}>
<Route path="/sign-with-ethereum" element={<SignWithEthereum />} /> <Route path="/sign-with-ethereum" element={<SignWithEthereum />} />
<Route path="/sign-with-cosmos/:ethAddress/:ethSignature" element={<SignWithCosmos />} /> <Route
path="/sign-with-cosmos/:cosmosAddress/:ethSignature"
element={<SignWithCosmos />}
/>
</Route> </Route>
<Route path="*" element={<PageNotFound />} /> <Route path="*" element={<PageNotFound />} />
</Routes> </Routes>
</WalletConnectProvider > </WalletConnectProvider>
</Router> </Router>
); );
} }

View File

@ -48,7 +48,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 isSignClientInitializing = useRef<boolean>(false); const isSignClientInitializing = useRef<boolean>(false);
const navigate = useNavigate() const navigate = useNavigate();
const disconnect = useCallback(async () => { const disconnect = useCallback(async () => {
if (signClient && session) { if (signClient && session) {
@ -80,7 +80,7 @@ export const WalletConnectProvider = ({
client.on("session_delete", () => { client.on("session_delete", () => {
setSession(null); setSession(null);
navigate("/") navigate("/");
}); });
}, },
[navigate] [navigate]
@ -91,10 +91,10 @@ export const WalletConnectProvider = ({
const signClient = await SignClient.init({ const signClient = await SignClient.init({
projectId: PROJECT_ID, projectId: PROJECT_ID,
metadata: { metadata: {
name: "Urbit onboarding app", name: "Testnet onboarding app",
description: "Urbit onboarding app", description: "Testnet onboarding app",
url: "localhost:3000", url: "localhost:3000",
icons: ["https://avatars.githubusercontent.com/u/5237680?s=200&v=4"], icons: ["https://avatars.githubusercontent.com/u/92608123"],
}, },
}); });
@ -102,7 +102,7 @@ export const WalletConnectProvider = ({
await subscribeToEvents(signClient); await subscribeToEvents(signClient);
await checkPersistedState(signClient); await checkPersistedState(signClient);
isSignClientInitializing.current = false; isSignClientInitializing.current = false;
}, [checkPersistedState, subscribeToEvents]) }, [checkPersistedState, subscribeToEvents]);
const connect = async () => { const connect = async () => {
if (!signClient) { if (!signClient) {
@ -114,10 +114,12 @@ export const WalletConnectProvider = ({
methods: ["personal_sign"], methods: ["personal_sign"],
chains: ["eip155:1"], chains: ["eip155:1"],
events: [], events: [],
},cosmos: { },
methods: cosmos: {
["cosmos_signDirect", "cosmos_signAmino", "cosmos_sendTransaction"], methods: [
chains: ["cosmos:cosmoshub-4", "cosmos:laconic_9000-1"], "cosmos_sendTransaction",
],
chains: ["cosmos:cosmoshub-4", "cosmos:laconic_9000-1"], // TODO: Get chain ID from .env
events: [], events: [],
}, },
}; };

View File

@ -8,7 +8,7 @@ import {
Button, Button,
Typography, Typography,
Container, Container,
Box Box,
} from "@mui/material"; } from "@mui/material";
import { useWalletConnectContext } from "../context/WalletConnectContext"; import { useWalletConnectContext } from "../context/WalletConnectContext";
@ -25,19 +25,19 @@ const SignPageLayout = () => {
return ( return (
<> <>
<Toolbar variant="dense"> <Toolbar variant="dense">
<Link to="/" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/" style={{ color: "inherit", textDecoration: "none" }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: "flex", alignItems: "center" }}>
<Avatar <Avatar
alt="Urbit logo" alt="Laconic logo"
src="https://avatars.githubusercontent.com/u/5237680?s=200&v=4" src="https://avatars.githubusercontent.com/u/92608123"
/> />
<IconButton <IconButton
edge="start" edge="start"
color="inherit" color="inherit"
aria-label="menu" aria-label="menu"
sx={{ mr: 2 }} sx={{ ml: 2, mr: 2 }}
> >
Urbit Testnet Onboarding
</IconButton> </IconButton>
</Box> </Box>
</Link> </Link>
@ -57,7 +57,13 @@ const SignPageLayout = () => {
<Container maxWidth="md"> <Container maxWidth="md">
{session && ( {session && (
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", flexDirection: "row", alignItems: 'flex-end' }}> <div
style={{
display: "flex",
flexDirection: "row",
alignItems: "flex-end",
}}
>
<Typography variant="body2"> <Typography variant="body2">
Connected to: <b> {session.peer.metadata.name}</b>{" "} Connected to: <b> {session.peer.metadata.name}</b>{" "}
</Typography> </Typography>
@ -65,7 +71,12 @@ const SignPageLayout = () => {
variant="square" variant="square"
alt="Peer logo" alt="Peer logo"
src={session.peer.metadata.icons[0]} src={session.peer.metadata.icons[0]}
sx={{ width: 20, height: 20, marginLeft: 1, paddingBottom: 0.5 }} sx={{
width: 20,
height: 20,
marginLeft: 1,
paddingBottom: 0.5,
}}
/> />
</div> </div>
<Typography variant="body2"> <Typography variant="body2">

View File

@ -1,38 +1,56 @@
import React, { useEffect } from 'react'; import React, { useEffect } from "react";
import { useNavigate } from 'react-router-dom' import { useNavigate } from "react-router-dom";
import { Typography, Button, Box, Container, Avatar } from '@mui/material'; import { Typography, Button, Box, Container, Avatar } from "@mui/material";
import { useWalletConnectContext } from "../context/WalletConnectContext"; import { useWalletConnectContext } from "../context/WalletConnectContext";
const ConnectWallet = () => { const ConnectWallet = () => {
const { connect, session } = useWalletConnectContext(); const { connect, session } = useWalletConnectContext();
const navigate = useNavigate() const navigate = useNavigate();
useEffect(() => { useEffect(() => {
if (session) { if (session) {
navigate("/sign-with-ethereum") navigate("/sign-with-ethereum");
} }
}, [session, navigate]) }, [session, navigate]);
const handler = async () => { const handler = async () => {
await connect(); await connect();
} };
return ( return (
<Container maxWidth="lg"> <Container maxWidth="lg">
<Box display="flex" flexDirection="column" alignItems="center" height="50vh" justifyContent="center" padding={5}> <Box
display="flex"
flexDirection="column"
alignItems="center"
height="50vh"
justifyContent="center"
padding={5}
>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Avatar alt="Urbit logo" src="https://avatars.githubusercontent.com/u/5237680?s=200&v=4" /> <Avatar
<Typography variant="h4" component="h6"> alt="Laconic logo"
Urbit Onboarding src="https://avatars.githubusercontent.com/u/92608123"
/>
<Typography
variant="h4"
component="h6"
style={{ marginLeft: "10px" }}
>
Testnet Onboarding
</Typography> </Typography>
</Box> </Box>
<Typography variant="h6" component="h6" style={{ marginTop: '30px' }}> <Typography variant="h6" component="h6" style={{ marginTop: "30px" }}>
Connect wallet Connect wallet
</Typography> </Typography>
<Button variant="contained" onClick={handler} style={{ marginTop: '20px' }}> <Button
variant="contained"
onClick={handler}
style={{ marginTop: "20px" }}
>
Connect Connect
</Button> </Button>
</Box> </Box>

View File

@ -1,141 +1,87 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { useParams, useLocation } from "react-router-dom"; import { useParams, useLocation } from "react-router-dom";
import { SnackbarProvider, enqueueSnackbar } from "notistack"; import { SnackbarProvider, enqueueSnackbar } from "notistack";
import canonicalStringify from "canonical-json";
import { import { Box, Typography } from "@mui/material";
Button,
Dialog,
DialogContent,
DialogActions,
Box,
Typography,
} from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton/LoadingButton"; import LoadingButton from "@mui/lab/LoadingButton/LoadingButton";
// TODO: Import types exported from registry-sdk
import { import {
MsgCreateBondEncodeObject, MsgOnboardParticipantEncodeObject,
} from "@cerc-io/registry-sdk/dist/types/cerc/bond/message"; typeUrlMsgOnboardParticipant,
} from "@cerc-io/registry-sdk";
import { useWalletConnectContext } from "../context/WalletConnectContext"; import { useWalletConnectContext } from "../context/WalletConnectContext";
const SignWithCosmos = () => { const SignWithCosmos = () => {
const { session, signClient } = useWalletConnectContext(); const { session, signClient } = useWalletConnectContext();
const { ethAddress, ethSignature } = useParams(); const { cosmosAddress, ethSignature } = useParams();
const [openModal, setOpenModal] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [cosmosSignature, setCosmosSignature] = useState("");
const location = useLocation(); const location = useLocation();
const innerMessage = location.state; const innerMessage = location.state;
const cosmosAddress = innerMessage.address; const ethAddress = innerMessage.address;
const displayAttestation = useMemo(() => { const onboardParticipantMsg: MsgOnboardParticipantEncodeObject =
return canonicalStringify( useMemo(() => {
{ return {
payload: { typeUrl: typeUrlMsgOnboardParticipant,
msg: "Onboarding my Azimuth ID onto UrbitChain", value: {
address: ethAddress, participant: cosmosAddress,
payload: innerMessage, ethPayload: innerMessage,
ethSignature,
}, },
signatures: [cosmosSignature, ethSignature], };
}, }, [cosmosAddress, innerMessage, ethSignature]);
null,
2
);
}, [ethAddress, cosmosSignature, ethSignature, innerMessage]);
const message = useMemo(() => { const sendTransaction = async (
return { transactionMessage: MsgOnboardParticipantEncodeObject
msg: "Onboarding my Azimuth ID onto UrbitChain", ) => {
address: ethAddress,
payload: innerMessage,
};
}, [ethAddress, innerMessage]);
const signCosmos = async () => {
if (!ethAddress) { if (!ethAddress) {
enqueueSnackbar("Set ethereum address");
return; return;
} }
try { try {
setIsLoading(true); setIsLoading(true);
const signDoc = {
msgs: [],
fee: { amount: [], gas: "23" },
chain_id: "cosmos:cosmoshub-4",
memo: canonicalStringify(message),
account_number: "7",
sequence: "54",
};
const params = { signerAddress: cosmosAddress, signDoc }; const params = { transactionMessage, signer: cosmosAddress };
const responseFromWallet = await signClient!.request<{
const signedMessage = await signClient!.request<{ signature: string }>({ code: number;
}>({
topic: session!.topic, topic: session!.topic,
chainId: "cosmos:cosmoshub-4", chainId: "cosmos:laconic_9000-1", // TODO: Get chain ID from .env
request: { request: {
method: "cosmos_signAmino", method: "cosmos_sendTransaction",
params, params,
}, },
}); });
setIsLoading(false); if (responseFromWallet.code !== 0) {
setOpenModal(true); enqueueSnackbar("Transaction not sent", { variant: "error" });
setCosmosSignature(signedMessage.signature); } else {
} catch (error) { enqueueSnackbar("Transaction successful", { variant: "success" });
setIsLoading(false);
setOpenModal(false);
enqueueSnackbar("Error signing message", { variant: "error" });
}
};
const sendTransaction = async (
transactionMessage: MsgCreateBondEncodeObject
) => {
try {
if (ethAddress) {
setIsLoading(true);
const params = { transactionMessage };
const responseFromWallet = await signClient!.request<{
code: number
}>({
topic: session!.topic,
chainId: "cosmos:laconic_9000-1", // TODO: Get chain from WalletConnect
request: {
method: "cosmos_sendTransaction",
params,
},
});
if (responseFromWallet.code !== 0) {
enqueueSnackbar("Error creating bond", { variant: "error" });
} else {
enqueueSnackbar("Created bond", { variant: "success" });
}
} }
} catch (error) { } catch (error) {
console.error(error);
enqueueSnackbar("Error in sending transaction", { variant: "error" }); enqueueSnackbar("Error in sending transaction", { variant: "error" });
} finally {
setIsLoading(false);
} }
setIsLoading(false);
}; };
// TODO: Add method to create attestation
return ( return (
<Box <Box
style={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
marginTop: "100px", marginTop: "100px",
gap: "10px", gap: "10px",
}} }}
> >
<Typography variant="h5">Sign with cosmos key</Typography> <Typography variant="h5">Send transaction to chain</Typography>
<Typography variant="body1">Cosmos account: {cosmosAddress}</Typography> <Typography variant="body1">Cosmos account: {cosmosAddress}</Typography>
<Typography variant="body1"> <Typography variant="body1">
Message: <br /> Onboarding message: <br />
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@ -145,49 +91,21 @@ const SignWithCosmos = () => {
}} }}
> >
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}> <pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{canonicalStringify(message, null, 2)}{" "} {JSON.stringify(onboardParticipantMsg, null, 2)}{" "}
</pre> </pre>
</Box> </Box>
<Box> <Box>
<LoadingButton <LoadingButton
variant="contained" variant="contained"
onClick={() => { onClick={async () => {
signCosmos(); await sendTransaction(onboardParticipantMsg);
}} }}
loading={isLoading} loading={isLoading}
> >
Send transaction Send transaction
</LoadingButton> </LoadingButton>
</Box> </Box>
<Dialog
open={openModal}
onClose={() => {
setOpenModal(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth="md"
>
<DialogContent>
Response from chain
<Box
sx={{
backgroundColor: "lightgray",
padding: 3,
wordWrap: "break-word",
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{displayAttestation}{" "}
</pre>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenModal(false)}>Close</Button>
</DialogActions>
</Dialog>
<SnackbarProvider /> <SnackbarProvider />
</Box> </Box>
); );

View File

@ -1,16 +1,11 @@
import React, { useState, useMemo, useEffect, useCallback } from "react"; import React, { useState, useMemo, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { SnackbarProvider, enqueueSnackbar } from "notistack"; import { SnackbarProvider, enqueueSnackbar } from "notistack";
import canonicalStringify from "canonical-json"; import canonicalStringify from "canonical-json";
import { import {
Button,
Select, Select,
MenuItem, MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box, Box,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
@ -35,15 +30,14 @@ const SignWithEthereum = () => {
const [cosmosAddress, setCosmosAddress] = useState(""); const [cosmosAddress, setCosmosAddress] = useState("");
const [openModal, setOpenModal] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const message = useMemo(() => { const message = useMemo(() => {
return { return {
msg: "Onboarding my cosmos validator onto UrbitChain", msg: "Register my account as a validator on the Laconic network",
address: cosmosAddress, address: ethAddress,
}; };
}, [cosmosAddress]); }, [ethAddress]);
const signEth = async () => { const signEth = async () => {
if (session && signClient) { if (session && signClient) {
@ -51,7 +45,7 @@ const SignWithEthereum = () => {
setIsLoading(true) setIsLoading(true)
const jsonMessage = canonicalStringify(message); const jsonMessage = canonicalStringify(message);
const hexMsg = utf8ToHex(jsonMessage, true); const hexMsg = utf8ToHex(jsonMessage, true);
const ethSignature: string = await signClient!.request({ const receivedEthSig: string = await signClient!.request({
topic: session!.topic, topic: session!.topic,
chainId: "eip155:1", chainId: "eip155:1",
request: { request: {
@ -61,22 +55,17 @@ const SignWithEthereum = () => {
}); });
setIsLoading(false) setIsLoading(false)
setEthSignature(ethSignature); setEthSignature(ethSignature);
setOpenModal(true); navigate(`/sign-with-cosmos/${cosmosAddress}/${receivedEthSig}`, {
state: message,
});
} catch (error) { } catch (error) {
console.log("err in signing ", error); console.log("err in signing ", error);
setIsLoading(false) setIsLoading(false)
setOpenModal(false);
enqueueSnackbar("Error signing message", { variant: "error" }); enqueueSnackbar("Error signing message", { variant: "error" });
} }
} }
}; };
const submitHandler = useCallback(() => {
navigate(`/sign-with-cosmos/${ethAddress}/${ethSignature}`, {
state: message,
});
setOpenModal(false);
}, [ethAddress, ethSignature, navigate, message]);
return ( return (
<div> <div>
@ -143,42 +132,6 @@ const SignWithEthereum = () => {
Sign using Ethereum key Sign using Ethereum key
</LoadingButton> </LoadingButton>
</Box> </Box>
<Dialog
open={openModal}
onClose={() => {
setOpenModal(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth="md"
>
<DialogTitle id="alert-dialog-title">
{"Signed message with ethereum key"}
</DialogTitle>
<DialogContent>
<Typography variant="body1" style={{ wordWrap: "break-word" }}>
Signature: {ethSignature}
</Typography>
<Box
sx={{
backgroundColor: "lightgray",
padding: 3,
wordWrap: "break-word",
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{canonicalStringify(message, null, 2)}{" "}
</pre>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={submitHandler} autoFocus>
Sign with cosmos
</Button>
<Button onClick={() => setOpenModal(false)}>Close</Button>
</DialogActions>
</Dialog>
<SnackbarProvider /> <SnackbarProvider />
</Box> </Box>
) : ( ) : (