From 6bdaf60ff4d489bb2403a6168501c2b9f5d3a4ba Mon Sep 17 00:00:00 2001 From: Adwait Gharpure <69599306+Adw8@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:52:53 +0530 Subject: [PATCH] Implement persisting session (#5) * Add layout to pages * Persist session * Style sign with ethereum page * Refactor modal * Remove unused styles * Implement disconnect session functionality * Add info about wallet in navbar * Remove unused imports * Use canonical JSON * Add line * Remove buffer from sign with ethereum page * Display signed message in dialog * Show modal on signature * Format cosmos signature * Add code block style for json messages * Display signature in first modal * Add urbit logo for connect wallet page * Display message for sign with ethereum * Handle review changes * Keep icon and text on same line --------- Co-authored-by: neeraj Co-authored-by: Adw8 --- package.json | 1 + src/App.tsx | 9 +- src/context/WalletConnectContext.tsx | 40 ++++- src/decs.d.ts | 1 + src/layout/SignPageLayout.tsx | 76 ++++++++ src/pages/ConnectWallet.tsx | 36 +++- src/pages/SignWithCosmos.tsx | 161 ++++++++++------- src/pages/SignWithEthereum.tsx | 255 +++++++++++++++------------ yarn.lock | 5 + 9 files changed, 396 insertions(+), 188 deletions(-) create mode 100644 src/decs.d.ts create mode 100644 src/layout/SignPageLayout.tsx diff --git a/package.json b/package.json index 0f70a04..0981820 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@walletconnect/types": "^2.11.3", "assert": "^2.1.0", "buffer": "^6.0.3", + "canonical-json": "^0.0.4", "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/App.tsx b/src/App.tsx index 1c77d29..d99418b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,21 @@ 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"; import SignWithCosmos from "./pages/SignWithCosmos"; import PageNotFound from "./pages/PageNotFound"; +import SignPageLayout from "./layout/SignPageLayout"; function App() { return ( } /> - } /> - } /> + } > + } /> + } /> + } /> diff --git a/src/context/WalletConnectContext.tsx b/src/context/WalletConnectContext.tsx index 25b7e6f..0af0f3c 100644 --- a/src/context/WalletConnectContext.tsx +++ b/src/context/WalletConnectContext.tsx @@ -5,11 +5,13 @@ import React, { ReactNode, useState, useEffect, + useCallback, } from "react"; import SignClient from "@walletconnect/sign-client"; import { WalletConnectModal } from "@walletconnect/modal"; import { SessionTypes } from "@walletconnect/types"; +import { getSdkError } from "@walletconnect/utils"; const PROJECT_ID = process.env.REACT_APP_WALLET_CONNECT_ID; assert(PROJECT_ID, "Wallet connect project id not provided"); @@ -18,12 +20,16 @@ interface ContextValue { connect: () => Promise; session: SessionTypes.Struct | null; signClient: SignClient | undefined; + checkPersistedState: (client: SignClient)=> Promise + disconnect: () => Promise; } const walletConnectContext = createContext({ connect: () => Promise.resolve(), session: null, signClient: undefined, + checkPersistedState: ()=> Promise.resolve(), + disconnect: () => Promise.resolve(), }); const web3Modal = new WalletConnectModal({ @@ -40,7 +46,28 @@ export const WalletConnectProvider = ({ const [signClient, setSignClient] = useState(); const [session, setSession] = useState(null); - const createClient = async () => { + const disconnect = useCallback(async () => { + if (signClient && session) { + await signClient.disconnect({ + topic: session.topic, + reason: getSdkError("USER_DISCONNECTED"), + }); + } + + setSession(null) + }, [signClient, session]); + + const checkPersistedState = useCallback(async(client: SignClient)=>{ + if (client.session.length) { + const lastKeyIndex = client.session.keys.length - 1; + const session = client.session.get( + client.session.keys[lastKeyIndex] + ); + setSession(session) + } + }, []) + + const createClient = useCallback(async () => { const signClient = await SignClient.init({ projectId: PROJECT_ID, metadata: { @@ -52,7 +79,8 @@ export const WalletConnectProvider = ({ }); setSignClient(signClient); - }; + await checkPersistedState(signClient) + }, [checkPersistedState]) const connect = async () => { if (!signClient) { @@ -88,7 +116,7 @@ export const WalletConnectProvider = ({ if (!signClient) { createClient(); } - }, [signClient]); + }, [signClient, createClient]); return ( {children} @@ -108,11 +138,15 @@ export const useWalletConnectContext = () => { connect, session, signClient, + checkPersistedState, + disconnect } = useContext(walletConnectContext); return { connect, session, signClient, + checkPersistedState, + disconnect }; }; diff --git a/src/decs.d.ts b/src/decs.d.ts new file mode 100644 index 0000000..51af1b3 --- /dev/null +++ b/src/decs.d.ts @@ -0,0 +1 @@ +declare module 'canonical-json' diff --git a/src/layout/SignPageLayout.tsx b/src/layout/SignPageLayout.tsx new file mode 100644 index 0000000..d417145 --- /dev/null +++ b/src/layout/SignPageLayout.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { Outlet, useNavigate } from "react-router-dom"; + +import { + Toolbar, + IconButton, + Avatar, + Button, + Typography, + Container, +} from "@mui/material"; + +import { useWalletConnectContext } from "../context/WalletConnectContext"; + +const SignPageLayout = () => { + const { disconnect, session } = useWalletConnectContext(); + const navigate = useNavigate(); + + const disconnectHandler = async () => { + await disconnect(); + navigate("/"); + }; + + return ( + <> + + + + Urbit + + + + + + + {session && ( +
+
+ + Connected to: {session.peer.metadata.name}{" "} + + +
+ + Session ID: {session.topic} + +
+ )} + +
+ + ); +}; + +export default SignPageLayout; diff --git a/src/pages/ConnectWallet.tsx b/src/pages/ConnectWallet.tsx index a477633..04613dd 100644 --- a/src/pages/ConnectWallet.tsx +++ b/src/pages/ConnectWallet.tsx @@ -1,22 +1,46 @@ import React from 'react'; import { useNavigate } from 'react-router-dom' +import { Typography, Button, Box, Grid, Avatar } from '@mui/material'; + import { useWalletConnectContext } from "../context/WalletConnectContext"; -// TODO: Connect wallet should not be accessible if session already exists const ConnectWallet = () => { - const { connect } = useWalletConnectContext(); + const { connect, session } = useWalletConnectContext(); + const navigate = useNavigate() + + if (session) { + navigate("/sign-with-ethereum") + } const handler = async () => { await connect(); navigate("/sign-with-ethereum") } return ( -
-

Connect wallet

- -
+ + + + + + + + + Urbit Onboarding + + + + Connect wallet + + + + + + + ); }; diff --git a/src/pages/SignWithCosmos.tsx b/src/pages/SignWithCosmos.tsx index 22e95c2..b5bb1e9 100644 --- a/src/pages/SignWithCosmos.tsx +++ b/src/pages/SignWithCosmos.tsx @@ -1,44 +1,59 @@ import React, { useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { SnackbarProvider, enqueueSnackbar } from "notistack"; +import canonicalStringify from "canonical-json"; -import { Modal, Button, Typography, Box } from "@mui/material"; -import Alert from "@mui/material/Alert"; -import CheckIcon from "@mui/icons-material/Check"; +import { + Button, + Dialog, + DialogContent, + DialogActions, + Box, + Typography, +} from "@mui/material"; import { useWalletConnectContext } from "../context/WalletConnectContext"; -const style = { - position: "absolute" as const, - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - overflow: "scroll", - bgcolor: "background.paper", - border: "2px solid #000", - boxShadow: 24, - p: 4, -}; - const SignWithCosmos = () => { const { session, signClient } = useWalletConnectContext(); const { ethAddress, cosmosAddress, ethSignature } = useParams(); const [openModal, setOpenModal] = useState(false); - const [result, setResult] = useState(""); + const [cosmosSignature, setCosmosSignature] = useState(""); - const message = useMemo(() => { - return JSON.stringify( + const displayAttestation = useMemo(() => { + return canonicalStringify( { - ethAddress, - ethSignature, - text: "Attested by ethereum key", + payload: { + msg: "Onboarding my Azimuth ID onto UrbitChain", + address: ethAddress, + payload: { + msg: "Onboarding my cosmos validator onto UrbitChain", + address: cosmosAddress, + }, + }, + signatures: [cosmosSignature, ethSignature], }, null, 2 ); - }, [ethAddress, ethSignature]); + }, [ethAddress, cosmosAddress, cosmosSignature, ethSignature]); + + const message = useMemo(() => { + return canonicalStringify( + { + msg: "Onboarding my Azimuth ID onto UrbitChain", + address: ethAddress, + payload: { + msg: "Onboarding my cosmos validator onto UrbitChain", + address: cosmosAddress, + }, + }, + null, + 2 + ); + }, [ethAddress, cosmosAddress]); const signCosmos = async () => { try { @@ -62,8 +77,8 @@ const SignWithCosmos = () => { params, }, }); - setOpenModal(false); - setResult(signedMessage.signature); + setOpenModal(true); + setCosmosSignature(signedMessage.signature); } } catch (error) { console.log("err in signing ", error); @@ -73,57 +88,69 @@ const SignWithCosmos = () => { }; return ( -
-

Sign using Cosmos key

-

Cosmos account: {cosmosAddress}

- - {message} + + + + + + setOpenModal(false)} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" + onClose={() => { + setOpenModal(false); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + maxWidth="md" > - - - Sign with Cosmos - - Message to sign: - - {message} - -
+ Attested message to be broadcasted on chain + - - -
-
-
+
+              {displayAttestation}{" "}
+            
+ + + + + + - {result && ( - } severity="success"> - Signed message ({result}) will be broadcasted to the chain - - )} -
+ ); }; diff --git a/src/pages/SignWithEthereum.tsx b/src/pages/SignWithEthereum.tsx index ec5c5c0..91bae74 100644 --- a/src/pages/SignWithEthereum.tsx +++ b/src/pages/SignWithEthereum.tsx @@ -1,148 +1,185 @@ -import React, { useState, useMemo } from "react"; -import assert from "assert"; +import React, { useState, useMemo, useEffect, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { SnackbarProvider, enqueueSnackbar } from "notistack"; +import canonicalStringify from "canonical-json"; import { - Modal, Button, - Typography, - Box, Select, MenuItem, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Box, + Typography, } from "@mui/material"; import { utf8ToHex } from "@walletconnect/encoding"; import { useWalletConnectContext } from "../context/WalletConnectContext"; -const style = { - position: "absolute" as const, - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - overflow: "scroll", - bgcolor: "background.paper", - border: "2px solid #000", - boxShadow: 24, - p: 4, -}; - const SignWithEthereum = () => { - window.Buffer = Buffer; + const { session, signClient, checkPersistedState } = + useWalletConnectContext(); + + useEffect(() => { + if (signClient && !session) { + checkPersistedState(signClient); + } + }, [session, signClient, checkPersistedState]); - const { session, signClient } = useWalletConnectContext(); const navigate = useNavigate(); const [ethAddress, setEthAddress] = useState(""); + const [ethSignature, setEthSignature] = useState(""); + const [cosmosAddress, setCosmosAddress] = useState(""); const [openModal, setOpenModal] = useState(false); const message = useMemo(() => { return { - cosmosAddress, - text: "Onboarding azimuth ID onto urbit chain", + msg: "Onboarding my cosmos validator onto UrbitChain", + address: cosmosAddress, }; }, [cosmosAddress]); const signEth = async () => { - try { - const jsonMessage = JSON.stringify(message, null, 2); - const hexMsg = utf8ToHex(jsonMessage, true); - const ethSignature: string = await signClient!.request({ - topic: session!.topic, - chainId: "eip155:1", - request: { - method: "personal_sign", - params: [hexMsg, ethAddress], - }, - }); - navigate( - `/sign-with-cosmos/${ethAddress}/${cosmosAddress}/${ethSignature}` - ); - } catch (error) { - console.log("err in signing ", error); - setOpenModal(false); - enqueueSnackbar("Error signing message", { variant: "error" }) + if (session && signClient) { + try { + const jsonMessage = canonicalStringify(message); + const hexMsg = utf8ToHex(jsonMessage, true); + const ethSignature: string = await signClient!.request({ + topic: session!.topic, + chainId: "eip155:1", + request: { + method: "personal_sign", + params: [hexMsg, ethAddress], + }, + }); + setEthSignature(ethSignature); + setOpenModal(true); + } catch (error) { + console.log("err in signing ", error); + setOpenModal(false); + enqueueSnackbar("Error signing message", { variant: "error" }); + } } }; - assert(session, "Session not found"); + const submitHandler = useCallback(() => { + navigate( + `/sign-with-cosmos/${ethAddress}/${cosmosAddress}/${ethSignature}` + ); + setOpenModal(false); + }, [ethAddress, cosmosAddress, ethSignature, navigate]); return (
-

Connected

-

Session id: {session.topic}

-

Select Cosmos accounts:

- -

Select Ethereum account:

- + {session ? ( + + Sign with ethereum key + Select Cosmos account: + + Select Ethereum account: + - - setOpenModal(false)} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" - > - - - Sign using ethereum ({ethAddress}) - - Message to sign: - - {JSON.stringify(message, null, 2)} - -
- - -
+
{JSON.stringify(message, null, 2)} 
+
)} + + + + { + setOpenModal(false); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + maxWidth="md" + > + + {"Signed message with ethereum key"} + + + + Signature: {ethSignature} + + + +
+                  {canonicalStringify(message, null, 2)}{" "}
+                
+
+
+ + + + +
+
- - + ) : ( + <>Loading... + )}
); }; diff --git a/yarn.lock b/yarn.lock index f2006d6..c405ab9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4100,6 +4100,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001591: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz#571cf4f3f1506df9bf41fcbb6d10d5d017817bce" integrity sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA== +canonical-json@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/canonical-json/-/canonical-json-0.0.4.tgz#6579c072c3db5c477ec41dc978fbf2b8f41074a3" + integrity sha512-2sW7x0m/P7dqEnO0O87U7RTVQAaa7MELcd+Jd9FA6CYgYtwJ1TlDWIYMD8nuMkH1KoThsJogqgLyklrt9d/Azw== + case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"