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 { 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 SignWithEthereum from "./pages/SignWithEthereum";
@ -14,13 +14,16 @@ function App() {
<WalletConnectProvider>
<Routes>
<Route path="/" element={<ConnectWallet />} />
<Route element={<SignPageLayout />} >
<Route element={<SignPageLayout />}>
<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 path="*" element={<PageNotFound />} />
</Routes>
</WalletConnectProvider >
</WalletConnectProvider>
</Router>
);
}

View File

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

View File

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

View File

@ -1,38 +1,56 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'
import React, { useEffect } from "react";
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";
const ConnectWallet = () => {
const { connect, session } = useWalletConnectContext();
const navigate = useNavigate()
const navigate = useNavigate();
useEffect(() => {
if (session) {
navigate("/sign-with-ethereum")
navigate("/sign-with-ethereum");
}
}, [session, navigate])
}, [session, navigate]);
const handler = async () => {
await connect();
}
};
return (
<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">
<Avatar alt="Urbit logo" src="https://avatars.githubusercontent.com/u/5237680?s=200&v=4" />
<Typography variant="h4" component="h6">
Urbit Onboarding
<Avatar
alt="Laconic logo"
src="https://avatars.githubusercontent.com/u/92608123"
/>
<Typography
variant="h4"
component="h6"
style={{ marginLeft: "10px" }}
>
Testnet Onboarding
</Typography>
</Box>
<Typography variant="h6" component="h6" style={{ marginTop: '30px' }}>
<Typography variant="h6" component="h6" style={{ marginTop: "30px" }}>
Connect wallet
</Typography>
<Button variant="contained" onClick={handler} style={{ marginTop: '20px' }}>
<Button
variant="contained"
onClick={handler}
style={{ marginTop: "20px" }}
>
Connect
</Button>
</Box>

View File

@ -1,141 +1,87 @@
import React, { useMemo, useState } from "react";
import { useParams, useLocation } from "react-router-dom";
import { SnackbarProvider, enqueueSnackbar } from "notistack";
import canonicalStringify from "canonical-json";
import {
Button,
Dialog,
DialogContent,
DialogActions,
Box,
Typography,
} from "@mui/material";
import { Box, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton/LoadingButton";
// TODO: Import types exported from registry-sdk
import {
MsgCreateBondEncodeObject,
} from "@cerc-io/registry-sdk/dist/types/cerc/bond/message";
MsgOnboardParticipantEncodeObject,
typeUrlMsgOnboardParticipant,
} from "@cerc-io/registry-sdk";
import { useWalletConnectContext } from "../context/WalletConnectContext";
const SignWithCosmos = () => {
const { session, signClient } = useWalletConnectContext();
const { ethAddress, ethSignature } = useParams();
const { cosmosAddress, ethSignature } = useParams();
const [openModal, setOpenModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [cosmosSignature, setCosmosSignature] = useState("");
const location = useLocation();
const innerMessage = location.state;
const cosmosAddress = innerMessage.address;
const ethAddress = innerMessage.address;
const displayAttestation = useMemo(() => {
return canonicalStringify(
{
payload: {
msg: "Onboarding my Azimuth ID onto UrbitChain",
address: ethAddress,
payload: innerMessage,
const onboardParticipantMsg: MsgOnboardParticipantEncodeObject =
useMemo(() => {
return {
typeUrl: typeUrlMsgOnboardParticipant,
value: {
participant: cosmosAddress,
ethPayload: innerMessage,
ethSignature,
},
signatures: [cosmosSignature, ethSignature],
},
null,
2
);
}, [ethAddress, cosmosSignature, ethSignature, innerMessage]);
};
}, [cosmosAddress, innerMessage, ethSignature]);
const message = useMemo(() => {
return {
msg: "Onboarding my Azimuth ID onto UrbitChain",
address: ethAddress,
payload: innerMessage,
};
}, [ethAddress, innerMessage]);
const signCosmos = async () => {
const sendTransaction = async (
transactionMessage: MsgOnboardParticipantEncodeObject
) => {
if (!ethAddress) {
enqueueSnackbar("Set ethereum address");
return;
}
try {
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 signedMessage = await signClient!.request<{ signature: string }>({
const params = { transactionMessage, signer: cosmosAddress };
const responseFromWallet = await signClient!.request<{
code: number;
}>({
topic: session!.topic,
chainId: "cosmos:cosmoshub-4",
chainId: "cosmos:laconic_9000-1", // TODO: Get chain ID from .env
request: {
method: "cosmos_signAmino",
method: "cosmos_sendTransaction",
params,
},
});
setIsLoading(false);
setOpenModal(true);
setCosmosSignature(signedMessage.signature);
} catch (error) {
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" });
}
if (responseFromWallet.code !== 0) {
enqueueSnackbar("Transaction not sent", { variant: "error" });
} else {
enqueueSnackbar("Transaction successful", { variant: "success" });
}
} catch (error) {
console.error(error);
enqueueSnackbar("Error in sending transaction", { variant: "error" });
} finally {
setIsLoading(false);
}
setIsLoading(false);
};
// TODO: Add method to create attestation
return (
<Box
style={{
sx={{
display: "flex",
flexDirection: "column",
marginTop: "100px",
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">
Message: <br />
Onboarding message: <br />
</Typography>
<Box
sx={{
@ -145,49 +91,21 @@ const SignWithCosmos = () => {
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{canonicalStringify(message, null, 2)}{" "}
{JSON.stringify(onboardParticipantMsg, null, 2)}{" "}
</pre>
</Box>
<Box>
<LoadingButton
variant="contained"
onClick={() => {
signCosmos();
onClick={async () => {
await sendTransaction(onboardParticipantMsg);
}}
loading={isLoading}
>
Send transaction
</LoadingButton>
</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 />
</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 { SnackbarProvider, enqueueSnackbar } from "notistack";
import canonicalStringify from "canonical-json";
import {
Button,
Select,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box,
Typography,
} from "@mui/material";
@ -35,15 +30,14 @@ const SignWithEthereum = () => {
const [cosmosAddress, setCosmosAddress] = useState("");
const [openModal, setOpenModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const message = useMemo(() => {
return {
msg: "Onboarding my cosmos validator onto UrbitChain",
address: cosmosAddress,
msg: "Register my account as a validator on the Laconic network",
address: ethAddress,
};
}, [cosmosAddress]);
}, [ethAddress]);
const signEth = async () => {
if (session && signClient) {
@ -51,7 +45,7 @@ const SignWithEthereum = () => {
setIsLoading(true)
const jsonMessage = canonicalStringify(message);
const hexMsg = utf8ToHex(jsonMessage, true);
const ethSignature: string = await signClient!.request({
const receivedEthSig: string = await signClient!.request({
topic: session!.topic,
chainId: "eip155:1",
request: {
@ -61,22 +55,17 @@ const SignWithEthereum = () => {
});
setIsLoading(false)
setEthSignature(ethSignature);
setOpenModal(true);
navigate(`/sign-with-cosmos/${cosmosAddress}/${receivedEthSig}`, {
state: message,
});
} catch (error) {
console.log("err in signing ", error);
setIsLoading(false)
setOpenModal(false);
enqueueSnackbar("Error signing message", { variant: "error" });
}
}
};
const submitHandler = useCallback(() => {
navigate(`/sign-with-cosmos/${ethAddress}/${ethSignature}`, {
state: message,
});
setOpenModal(false);
}, [ethAddress, ethSignature, navigate, message]);
return (
<div>
@ -143,42 +132,6 @@ const SignWithEthereum = () => {
Sign using Ethereum key
</LoadingButton>
</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 />
</Box>
) : (