From 13c369dcd976fedb3d68668e552b70f27d2d7a7c Mon Sep 17 00:00:00 2001 From: Diana Savatina Date: Tue, 1 Oct 2024 14:03:39 +0100 Subject: [PATCH 1/2] feat: WalletConnect integration, part 2, pairing list --- apps/desktop/src/Router.tsx | 4 +- apps/desktop/src/utils/beacon/BeaconPeers.tsx | 12 +- .../utils/beacon/PermissionRequestModal.tsx | 4 +- .../utils/beacon/useHandleBeaconMessage.tsx | 6 +- .../src/components/Menu/AppsMenu/AppsMenu.tsx | 3 +- .../WalletConnect/SessionProposalModal.tsx | 9 +- .../WalletConnect/WalletConnectPeers.tsx | 131 ++++++++++++++++++ .../WalletConnect/WalletConnectProvider.tsx | 14 +- .../src/components/WalletConnect/index.tsx | 1 + .../web/src/components/beacon/BeaconPeers.tsx | 10 +- .../beacon/PermissionRequestModal.tsx | 4 +- .../beacon/useHandleBeaconMessage.tsx | 4 +- packages/state/package.json | 1 + packages/state/src/hooks/WalletConnect.ts | 27 ++++ packages/state/src/hooks/beacon.test.ts | 20 +-- packages/state/src/hooks/beacon.ts | 41 +++--- packages/state/src/hooks/index.ts | 1 + .../src/hooks/removeAccountDependencies.ts | 4 +- packages/state/src/reducer.ts | 2 + packages/state/src/slices/WalletConnect.ts | 29 ++++ packages/state/src/slices/beacon.ts | 4 +- packages/state/src/slices/index.ts | 1 + pnpm-lock.yaml | 5 +- 23 files changed, 272 insertions(+), 65 deletions(-) create mode 100644 apps/web/src/components/WalletConnect/WalletConnectPeers.tsx create mode 100644 packages/state/src/hooks/WalletConnect.ts create mode 100644 packages/state/src/slices/WalletConnect.ts diff --git a/apps/desktop/src/Router.tsx b/apps/desktop/src/Router.tsx index aa81ff12fa..be27e89db9 100644 --- a/apps/desktop/src/Router.tsx +++ b/apps/desktop/src/Router.tsx @@ -1,7 +1,7 @@ /* istanbul ignore file */ import { DynamicModalContext, useDynamicModal } from "@umami/components"; import { useDataPolling } from "@umami/data-polling"; -import { WalletClient, useImplicitAccounts, useResetConnections } from "@umami/state"; +import { WalletClient, useImplicitAccounts, useResetBeaconConnections } from "@umami/state"; import { noop } from "lodash"; import { useEffect } from "react"; import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; @@ -59,7 +59,7 @@ const LoggedInRouterWithPolling = () => { }; const LoggedOutRouter = () => { - const resetBeaconConnections = useResetConnections(); + const resetBeaconConnections = useResetBeaconConnections(); useEffect(() => { WalletClient.destroy().then(resetBeaconConnections).catch(noop); diff --git a/apps/desktop/src/utils/beacon/BeaconPeers.tsx b/apps/desktop/src/utils/beacon/BeaconPeers.tsx index e6016208e4..2c8b86f887 100644 --- a/apps/desktop/src/utils/beacon/BeaconPeers.tsx +++ b/apps/desktop/src/utils/beacon/BeaconPeers.tsx @@ -10,7 +10,7 @@ import { Image, Text, } from "@chakra-ui/react"; -import { useGetConnectionInfo, usePeers, useRemovePeer } from "@umami/state"; +import { useBeaconPeers, useGetBeaconConnectionInfo, useRemoveBeaconPeer } from "@umami/state"; import { parsePkh } from "@umami/tezos"; import capitalize from "lodash/capitalize"; import { Fragment } from "react"; @@ -22,10 +22,10 @@ import colors from "../../style/colors"; /** * Component displaying a list of connected dApps. * - * Loads dApps data from {@link usePeers} hook & zips it with generated dAppIds. + * Loads dApps data from {@link useBeaconPeers} hook & zips it with generated dAppIds. */ export const BeaconPeers = () => { - const { peers } = usePeers(); + const { peers } = useBeaconPeers(); if (peers.length === 0) { return ( @@ -57,7 +57,7 @@ export const BeaconPeers = () => { * @param onRemove - action for deleting dApp connection. */ const PeerRow = ({ peerInfo }: { peerInfo: ExtendedPeerInfo }) => { - const removePeer = useRemovePeer(); + const removeBeaconPeer = useRemoveBeaconPeer(); return ( @@ -76,7 +76,7 @@ const PeerRow = ({ peerInfo }: { peerInfo: ExtendedPeerInfo }) => { } - onClick={() => removePeer(peerInfo)} + onClick={() => removeBeaconPeer(peerInfo)} size="xs" variant="circle" /> @@ -94,7 +94,7 @@ const PeerRow = ({ peerInfo }: { peerInfo: ExtendedPeerInfo }) => { * @param peerInfo - peerInfo provided by beacon Api + computed dAppId. */ const StoredPeerInfo = ({ peerInfo }: { peerInfo: ExtendedPeerInfo }) => { - const connectionInfo = useGetConnectionInfo(peerInfo.senderId); + const connectionInfo = useGetBeaconConnectionInfo(peerInfo.senderId); if (!connectionInfo) { return null; diff --git a/apps/desktop/src/utils/beacon/PermissionRequestModal.tsx b/apps/desktop/src/utils/beacon/PermissionRequestModal.tsx index ea75d6f4c4..75edc019eb 100644 --- a/apps/desktop/src/utils/beacon/PermissionRequestModal.tsx +++ b/apps/desktop/src/utils/beacon/PermissionRequestModal.tsx @@ -27,7 +27,7 @@ import { import { useDynamicModalContext } from "@umami/components"; import { WalletClient, - useAddConnection, + useAddBeaconConnection, useAsyncActionHandler, useGetImplicitAccount, } from "@umami/state"; @@ -39,7 +39,7 @@ import { OwnedImplicitAccountsAutocomplete } from "../../components/AddressAutoc import colors from "../../style/colors"; export const PermissionRequestModal = ({ request }: { request: PermissionRequestOutput }) => { - const addConnectionToBeaconSlice = useAddConnection(); + const addConnectionToBeaconSlice = useAddBeaconConnection(); const getAccount = useGetImplicitAccount(); const { onClose } = useDynamicModalContext(); const { handleAsyncAction } = useAsyncActionHandler(); diff --git a/apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx b/apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx index 2908b9ec31..df0048bb52 100644 --- a/apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx +++ b/apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx @@ -11,7 +11,7 @@ import { useAsyncActionHandler, useFindNetwork, useGetOwnedAccountSafe, - useRemovePeerBySenderId, + useRemoveBeaconPeerBySenderId, } from "@umami/state"; import { type Network } from "@umami/tezos"; @@ -23,7 +23,7 @@ import { BeaconSignPage } from "../../components/SendFlow/Beacon/BeaconSignPage" /** * @returns a function that handles a beacon message and opens a modal with the appropriate content * - * For operation requests it will also try to convert the operation(s) to our {@link Operation} format, + * For operation requests it will also try to convert the operation(s)n to our {@link Operation} format, * estimate the fee and open the BeaconSignPage only if it succeeds */ export const useHandleBeaconMessage = () => { @@ -31,7 +31,7 @@ export const useHandleBeaconMessage = () => { const { handleAsyncAction } = useAsyncActionHandler(); const getAccount = useGetOwnedAccountSafe(); const findNetwork = useFindNetwork(); - const removePeer = useRemovePeerBySenderId(); + const removePeer = useRemoveBeaconPeerBySenderId(); // we should confirm that we support the network that the beacon request is coming from const checkNetwork = ({ diff --git a/apps/web/src/components/Menu/AppsMenu/AppsMenu.tsx b/apps/web/src/components/Menu/AppsMenu/AppsMenu.tsx index 1bef31a11e..c278bba218 100644 --- a/apps/web/src/components/Menu/AppsMenu/AppsMenu.tsx +++ b/apps/web/src/components/Menu/AppsMenu/AppsMenu.tsx @@ -2,7 +2,7 @@ import { Button, Text } from "@chakra-ui/react"; import { useAddPeer } from "@umami/state"; import { BeaconPeers } from "../../beacon"; -import { useOnWalletConnect } from "../../WalletConnect"; +import { WcPeers, useOnWalletConnect } from "../../WalletConnect"; import { DrawerContentWrapper } from "../DrawerContentWrapper"; export const AppsMenu = () => { @@ -36,6 +36,7 @@ export const AppsMenu = () => { title="Apps" > + ); }; diff --git a/apps/web/src/components/WalletConnect/SessionProposalModal.tsx b/apps/web/src/components/WalletConnect/SessionProposalModal.tsx index d8d6176087..0122248b56 100644 --- a/apps/web/src/components/WalletConnect/SessionProposalModal.tsx +++ b/apps/web/src/components/WalletConnect/SessionProposalModal.tsx @@ -16,7 +16,12 @@ import { } from "@chakra-ui/react"; import { type WalletKitTypes } from "@reown/walletkit"; import { useDynamicModalContext } from "@umami/components"; -import { useAsyncActionHandler, useGetImplicitAccount, walletKit } from "@umami/state"; +import { + useAsyncActionHandler, + useGetImplicitAccount, + useToggleWcPeerListUpdated, + walletKit, +} from "@umami/state"; import { type SessionTypes } from "@walletconnect/types"; import { buildApprovedNamespaces, getSdkError } from "@walletconnect/utils"; import { FormProvider, useForm } from "react-hook-form"; @@ -35,6 +40,7 @@ export const SessionProposalModal = ({ network: NetworkType; }) => { const getAccount = useGetImplicitAccount(); + const toggleWcPeerListUpdated = useToggleWcPeerListUpdated(); const color = useColor(); const { onClose } = useDynamicModalContext(); @@ -71,6 +77,7 @@ export const SessionProposalModal = ({ sessionProperties: {}, }); console.log("WC session approved", session); + toggleWcPeerListUpdated(); onClose(); }); diff --git a/apps/web/src/components/WalletConnect/WalletConnectPeers.tsx b/apps/web/src/components/WalletConnect/WalletConnectPeers.tsx new file mode 100644 index 0000000000..6d094551a7 --- /dev/null +++ b/apps/web/src/components/WalletConnect/WalletConnectPeers.tsx @@ -0,0 +1,131 @@ +import { Center, Divider, Flex, IconButton, Image, Text, VStack } from "@chakra-ui/react"; +import { useDisconnectWalletConnectPeer, useGetWcPeerListToggle, walletKit } from "@umami/state"; +import { parsePkh } from "@umami/tezos"; +import { type SessionTypes } from "@walletconnect/types"; +import { getSdkError } from "@walletconnect/utils"; +import capitalize from "lodash/capitalize"; +import { useEffect, useState } from "react"; + +import { CodeSandboxIcon, StubIcon as TrashIcon } from "../../assets/icons"; +import { useColor } from "../../styles/useColor"; +import { AddressPill } from "../AddressPill"; +import { EmptyMessage } from "../EmptyMessage"; + +/** + * Component displaying a list of connected dApps. + * + * Loads dApps data from WalletConnect API and displays it in a list. + */ +export const WcPeers = () => { + const [sessions, setSessions] = useState>({}); + const isUpdated = useGetWcPeerListToggle(); + + useEffect(() => { + const sessions: Record = walletKit.getActiveSessions(); + setSessions(sessions); + }, [isUpdated]); + + if (!Object.keys(sessions).length) { + return ( + + ); + } + + return ( + } + spacing="0" + > + { + // loop peers and print PeerRow + Object.entries(sessions).map(([topic, sessionInfo]) => ( + + )) + } + + ); +}; + +/** + * Component for displaying info about single connected dApp. + * + * @param sessionInfo - sessionInfo provided by wc Api + computed dAppId. + * @param onRemove - action for deleting dApp connection. + */ +const PeerRow = ({ sessionInfo }: { sessionInfo: SessionTypes.Struct }) => { + const color = useColor(); + const disconnectWalletConnectPeer = useDisconnectWalletConnectPeer(); + + return ( +
+ +
+ } + src={sessionInfo.peer.metadata.icons[0]} + /> +
+
+ + {sessionInfo.peer.metadata.name} + + +
+
+ } + onClick={() => + disconnectWalletConnectPeer({ + topic: sessionInfo.topic, + reason: getSdkError("USER_DISCONNECTED"), + }) + } + variant="iconButtonSolid" + /> +
+ ); +}; + +/** + * Component for displaying additional info about connection with a dApp. + * + * Displays {@link AddressPill} with a connected account and network type. + * + * @param sessionInfo - sessionInfo provided by WalletConnect Api. + * Account is stored in format: tezos:ghostnet:tz1... + * Network is stored in format: tezos:mainnet + */ +const StoredSessionInfo = ({ sessionInfo }: { sessionInfo: SessionTypes.Struct }) => ( + + + + + Network: + + + {capitalize(sessionInfo.namespaces.tezos.chains?.[0].split(":")[1] ?? "")} + + +); diff --git a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx index cf72f55f36..e41f54f7c9 100644 --- a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx +++ b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx @@ -8,6 +8,7 @@ import { createWalletKit, useAsyncActionHandler, useAvailableNetworks, + useToggleWcPeerListUpdated, walletKit, } from "@umami/state"; import { type Network } from "@umami/tezos"; @@ -29,6 +30,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { const eventEmitters = useRef([]); const { handleAsyncActionUnsafe } = useAsyncActionHandler(); const { openWith } = useDynamicModalContext(); + const toggleWcPeerListUpdated = useToggleWcPeerListUpdated(); const toast = useToast(); const availableNetworks: Network[] = useAvailableNetworks(); @@ -59,7 +61,6 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { } await openWith(, {}); - console.log("Session proposal from dApp", proposal, walletKit.getActiveSessions()); }).catch(async () => { // dApp is waiting so we need to notify it await walletKit.rejectSession({ @@ -75,11 +76,13 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { // by that time the session is already deleted from WalletKit so we cannot find the dApp name console.log("WC session deleted by peer dApp", event); toast({ - description: "Session deleted by peer dApp", + description: "WalletConnect Session deleted by peer dApp", status: "info", }); + // now re-render peer list + toggleWcPeerListUpdated(); }, - [toast] + [toast, toggleWcPeerListUpdated] ); const onSessionRequest = useCallback( @@ -97,10 +100,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { description: `Session request from dApp ${session.peer.metadata.name}`, status: "info", }); - toast({ - description: "Request handling is not implemented yet. Rejecting the request.", - status: "error", - }); + throw new Error("Not implemented"); } catch (error) { const { id, topic } = event; const activeSessions: Record = walletKit.getActiveSessions(); diff --git a/apps/web/src/components/WalletConnect/index.tsx b/apps/web/src/components/WalletConnect/index.tsx index 1a17604707..bac0f36919 100644 --- a/apps/web/src/components/WalletConnect/index.tsx +++ b/apps/web/src/components/WalletConnect/index.tsx @@ -1 +1,2 @@ export * from "./WalletConnectProvider"; +export * from "./WalletConnectPeers"; diff --git a/apps/web/src/components/beacon/BeaconPeers.tsx b/apps/web/src/components/beacon/BeaconPeers.tsx index 192e551e49..904f0208c6 100644 --- a/apps/web/src/components/beacon/BeaconPeers.tsx +++ b/apps/web/src/components/beacon/BeaconPeers.tsx @@ -1,6 +1,6 @@ import { type ExtendedPeerInfo } from "@airgap/beacon-wallet"; import { Center, Divider, Flex, Heading, IconButton, Image, Text, VStack } from "@chakra-ui/react"; -import { useGetConnectionInfo, usePeers, useRemovePeer } from "@umami/state"; +import { useBeaconPeers, useGetBeaconConnectionInfo, useRemoveBeaconPeer } from "@umami/state"; import { parsePkh } from "@umami/tezos"; import capitalize from "lodash/capitalize"; @@ -12,10 +12,10 @@ import { EmptyMessage } from "../EmptyMessage"; /** * Component displaying a list of connected dApps. * - * Loads dApps data from {@link usePeers} hook & zips it with generated dAppIds. + * Loads dApps data from {@link useBeaconPeers} hook & zips it with generated dAppIds. */ export const BeaconPeers = () => { - const { peers } = usePeers(); + const { peers } = useBeaconPeers(); if (peers.length === 0) { return ( @@ -53,7 +53,7 @@ export const BeaconPeers = () => { */ const PeerRow = ({ peerInfo }: { peerInfo: ExtendedPeerInfo }) => { const color = useColor(); - const removePeer = useRemovePeer(); + const removePeer = useRemoveBeaconPeer(); return (
{ * @param peerInfo - peerInfo provided by beacon Api + computed dAppId. */ const StoredPeerInfo = ({ peerInfo }: { peerInfo: ExtendedPeerInfo }) => { - const connectionInfo = useGetConnectionInfo(peerInfo.senderId); + const connectionInfo = useGetBeaconConnectionInfo(peerInfo.senderId); if (!connectionInfo) { return null; diff --git a/apps/web/src/components/beacon/PermissionRequestModal.tsx b/apps/web/src/components/beacon/PermissionRequestModal.tsx index f1e6e3facd..6fdb382d66 100644 --- a/apps/web/src/components/beacon/PermissionRequestModal.tsx +++ b/apps/web/src/components/beacon/PermissionRequestModal.tsx @@ -26,7 +26,7 @@ import { import { useDynamicModalContext } from "@umami/components"; import { WalletClient, - useAddConnection, + useAddBeaconConnection, useAsyncActionHandler, useGetImplicitAccount, } from "@umami/state"; @@ -41,7 +41,7 @@ import { JsValueWrap } from "../JsValueWrap"; export const PermissionRequestModal = ({ request }: { request: PermissionRequestOutput }) => { const color = useColor(); - const addConnectionToBeaconSlice = useAddConnection(); + const addConnectionToBeaconSlice = useAddBeaconConnection(); const getAccount = useGetImplicitAccount(); const { onClose } = useDynamicModalContext(); const { handleAsyncAction } = useAsyncActionHandler(); diff --git a/apps/web/src/components/beacon/useHandleBeaconMessage.tsx b/apps/web/src/components/beacon/useHandleBeaconMessage.tsx index 2908b9ec31..be96589275 100644 --- a/apps/web/src/components/beacon/useHandleBeaconMessage.tsx +++ b/apps/web/src/components/beacon/useHandleBeaconMessage.tsx @@ -11,7 +11,7 @@ import { useAsyncActionHandler, useFindNetwork, useGetOwnedAccountSafe, - useRemovePeerBySenderId, + useRemoveBeaconPeerBySenderId, } from "@umami/state"; import { type Network } from "@umami/tezos"; @@ -31,7 +31,7 @@ export const useHandleBeaconMessage = () => { const { handleAsyncAction } = useAsyncActionHandler(); const getAccount = useGetOwnedAccountSafe(); const findNetwork = useFindNetwork(); - const removePeer = useRemovePeerBySenderId(); + const removePeer = useRemoveBeaconPeerBySenderId(); // we should confirm that we support the network that the beacon request is coming from const checkNetwork = ({ diff --git a/packages/state/package.json b/packages/state/package.json index 370cad3765..8ada31cf4c 100644 --- a/packages/state/package.json +++ b/packages/state/package.json @@ -83,6 +83,7 @@ "@umami/tzkt": "workspace:^", "@walletconnect/core": "^2.16.2", "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/types": "^2.16.2", "@walletconnect/utils": "^2.16.2", "bip39": "^3.1.0", "framer-motion": "^11.12.0", diff --git a/packages/state/src/hooks/WalletConnect.ts b/packages/state/src/hooks/WalletConnect.ts new file mode 100644 index 0000000000..8b82fb46a5 --- /dev/null +++ b/packages/state/src/hooks/WalletConnect.ts @@ -0,0 +1,27 @@ +import { type ErrorResponse } from "@walletconnect/jsonrpc-utils"; +import { useDispatch } from "react-redux"; + +import { useAppSelector } from "./useAppSelector"; +import { wcActions } from "../slices"; +import { walletKit } from "../walletConnect"; + +// get a toggle to monitor updates in peer list +export const useGetWcPeerListToggle = () => { + const wcData = useAppSelector(s => s.walletconnect); + return wcData.peerListUpdatedToggle; +}; + +// report that the peer list is updated +export const useToggleWcPeerListUpdated = () => { + const dispatch = useDispatch(); + return () => dispatch(wcActions.togglePeerListUpdated()); +}; + +// remove dApp connection from WalletConnect SDK +export const useDisconnectWalletConnectPeer = () => { + const togglePeerListUpdated = useToggleWcPeerListUpdated(); + return async (params: { topic: string; reason: ErrorResponse }) => { + await walletKit.disconnectSession(params).then(() => togglePeerListUpdated()); + console.log("WC session deleted on user request", params); + }; +}; diff --git a/packages/state/src/hooks/beacon.test.ts b/packages/state/src/hooks/beacon.test.ts index 649e27f039..85936430e8 100644 --- a/packages/state/src/hooks/beacon.test.ts +++ b/packages/state/src/hooks/beacon.test.ts @@ -3,10 +3,10 @@ import { mockMnemonicAccount, mockSocialAccount } from "@umami/core"; import { type RawPkh } from "@umami/tezos"; import { - useAddConnection, - useGetConnectionInfo, - useRemoveConnection, - useResetConnections, + useAddBeaconConnection, + useGetBeaconConnectionInfo, + useRemoveBeaconConnection, + useResetBeaconConnections, } from "./beacon"; import { beaconActions } from "../slices"; import { type UmamiStore, makeStore } from "../store"; @@ -33,7 +33,7 @@ const connectionInfo = (accountPkh: RawPkh, networkType: NetworkType) => ({ describe("useGetConnectedAccount", () => { it("returns undefined when no connection for dAppId is stored", () => { - const view = renderHook(() => useGetConnectionInfo(dAppId1), { store }); + const view = renderHook(() => useGetBeaconConnectionInfo(dAppId1), { store }); expect(view.result.current).toBeUndefined(); }); @@ -42,7 +42,7 @@ describe("useGetConnectedAccount", () => { addConnection(dAppId1, pkh1, NetworkType.MAINNET); addConnection(dAppId2, pkh2, NetworkType.GHOSTNET); - const view = renderHook(() => useGetConnectionInfo(dAppId2), { store }); + const view = renderHook(() => useGetBeaconConnectionInfo(dAppId2), { store }); expect(view.result.current).toEqual(connectionInfo(pkh2, NetworkType.GHOSTNET)); }); @@ -55,7 +55,7 @@ describe("useResetConnections", () => { const { result: { current: resetBeaconSlice }, - } = renderHook(() => useResetConnections(), { store }); + } = renderHook(() => useResetBeaconConnections(), { store }); resetBeaconSlice(); expect(store.getState().beacon).toEqual({}); @@ -68,7 +68,7 @@ describe("useAddConnection", () => { const { result: { current: addConnectionHook }, - } = renderHook(() => useAddConnection(), { store }); + } = renderHook(() => useAddBeaconConnection(), { store }); addConnectionHook(dAppId2, pkh2, NetworkType.MAINNET); expect(store.getState().beacon).toEqual({ @@ -82,7 +82,7 @@ describe("useAddConnection", () => { const { result: { current: addConnectionHook }, - } = renderHook(() => useAddConnection(), { store }); + } = renderHook(() => useAddBeaconConnection(), { store }); addConnectionHook(dAppId1, pkh2, NetworkType.MAINNET); expect(store.getState().beacon).toEqual({ @@ -98,7 +98,7 @@ describe("useRemoveConnection", () => { const { result: { current: removeConnection }, - } = renderHook(() => useRemoveConnection(), { store }); + } = renderHook(() => useRemoveBeaconConnection(), { store }); removeConnection(dAppId2); expect(store.getState().beacon).toEqual({ diff --git a/packages/state/src/hooks/beacon.ts b/packages/state/src/hooks/beacon.ts index b31f4e7881..1eb141a6b1 100644 --- a/packages/state/src/hooks/beacon.ts +++ b/packages/state/src/hooks/beacon.ts @@ -12,18 +12,21 @@ import { useDispatch } from "react-redux"; import { useAppSelector } from "./useAppSelector"; import { WalletClient, parsePeerInfo } from "../beacon"; -import { type DAppConnectionInfo, beaconActions } from "../slices"; +import { type DAppBeaconConnectionInfo, beaconActions } from "../slices"; + /** * Returns connected account pkh & network by a given dAppId. * * @param dAppId - generated from dApp public key. */ -export const useGetConnectionInfo = (dAppId: string): DAppConnectionInfo | undefined => { +export const useGetBeaconConnectionInfo = ( + dAppId: string +): DAppBeaconConnectionInfo | undefined => { const beaconConnections = useAppSelector(s => s.beacon); return beaconConnections[dAppId]; }; -export const useGetPeersForAccounts = () => { +export const useGetBeaconPeersForAccounts = () => { const beaconConnections = useAppSelector(s => s.beacon); return (pkhs: RawPkh[]) => @@ -37,7 +40,7 @@ export const useGetPeersForAccounts = () => { /** * Returns function for removing all connections from {@link beaconSlice}. */ -export const useResetConnections = () => { +export const useResetBeaconConnections = () => { const dispatch = useDispatch(); return () => dispatch(beaconActions.reset()); }; @@ -45,7 +48,7 @@ export const useResetConnections = () => { /** * Returns function for adding connection info to {@link beaconSlice}. */ -export const useAddConnection = () => { +export const useAddBeaconConnection = () => { const dispatch = useDispatch(); return (dAppId: string, accountPkh: RawPkh, networkType: NetworkType) => dispatch(beaconActions.addConnection({ dAppId, accountPkh, networkType })); @@ -54,17 +57,17 @@ export const useAddConnection = () => { /** * Returns function for removing connection from {@link beaconSlice}. */ -export const useRemoveConnection = () => { +export const useRemoveBeaconConnection = () => { const dispatch = useDispatch(); return (dAppId: string) => dispatch(beaconActions.removeConnection(dAppId)); }; -export const usePeers = () => { +export const useBeaconPeers = () => { const query = useQuery({ queryKey: ["beaconPeers"], queryFn: async () => { - const peers = await WalletClient.getPeers(); - return peers as ExtendedPeerInfo[]; + const beaconPeers: ExtendedPeerInfo[] = (await WalletClient.getPeers()) as ExtendedPeerInfo[]; + return beaconPeers; }, initialData: [], }); @@ -72,9 +75,9 @@ export const usePeers = () => { return { peers: query.data, refresh: query.refetch }; }; -export const useRemovePeer = () => { - const { refresh } = usePeers(); - const removeConnectionFromBeaconSlice = useRemoveConnection(); +export const useRemoveBeaconPeer = () => { + const { refresh } = useBeaconPeers(); + const removeConnectionFromBeaconSlice = useRemoveBeaconConnection(); return (peerInfo: ExtendedPeerInfo) => WalletClient.removePeer(peerInfo as ExtendedP2PPairingResponse, true) @@ -82,23 +85,23 @@ export const useRemovePeer = () => { .finally(() => void refresh()); }; -export const useRemovePeerBySenderId = () => { - const { peers } = usePeers(); - const removePeer = useRemovePeer(); +export const useRemoveBeaconPeerBySenderId = () => { + const { peers } = useBeaconPeers(); + const removePeer = useRemoveBeaconPeer(); return (senderId: string) => Promise.all(peers.filter(peerInfo => senderId === peerInfo.senderId).map(removePeer)); }; -export const useRemovePeersByAccounts = () => { - const getPeersForAccounts = useGetPeersForAccounts(); - const removePeerBySenderId = useRemovePeerBySenderId(); +export const useRemoveBeaconPeersByAccounts = () => { + const getPeersForAccounts = useGetBeaconPeersForAccounts(); + const removePeerBySenderId = useRemoveBeaconPeerBySenderId(); return (pkhs: RawPkh[]) => Promise.all(getPeersForAccounts(pkhs).map(removePeerBySenderId)); }; export const useAddPeer = () => { - const { refresh } = usePeers(); + const { refresh } = useBeaconPeers(); const toast = useToast(); return (payload: string) => diff --git a/packages/state/src/hooks/index.ts b/packages/state/src/hooks/index.ts index 5f5eeb14a7..b270250152 100644 --- a/packages/state/src/hooks/index.ts +++ b/packages/state/src/hooks/index.ts @@ -18,3 +18,4 @@ export * from "./removeAccountDependencies"; export * from "./setAccountData"; export * from "./staking"; export * from "./tokens"; +export * from "./WalletConnect"; diff --git a/packages/state/src/hooks/removeAccountDependencies.ts b/packages/state/src/hooks/removeAccountDependencies.ts index cbf6ff37ce..b62b2b0dce 100644 --- a/packages/state/src/hooks/removeAccountDependencies.ts +++ b/packages/state/src/hooks/removeAccountDependencies.ts @@ -1,6 +1,6 @@ import { type Account, type ImplicitAccount } from "@umami/core"; -import { useRemovePeersByAccounts } from "./beacon"; +import { useRemoveBeaconPeersByAccounts } from "./beacon"; import { useCurrentAccount, useImplicitAccounts } from "./getAccountData"; import { useMultisigAccounts } from "./multisig"; import { useAppDispatch } from "./useAppDispatch"; @@ -45,7 +45,7 @@ export const useRemoveDependenciesAndMultisigs = () => { */ const useRemoveAccountsDependencies = () => { const dispatch = useAppDispatch(); - const removePeersByAccounts = useRemovePeersByAccounts(); + const removePeersByAccounts = useRemoveBeaconPeersByAccounts(); const currentAccount = useCurrentAccount(); const implicitAccounts = useImplicitAccounts(); diff --git a/packages/state/src/reducer.ts b/packages/state/src/reducer.ts index ee824ed428..f516ea652f 100644 --- a/packages/state/src/reducer.ts +++ b/packages/state/src/reducer.ts @@ -4,6 +4,7 @@ import createWebStorage from "redux-persist/lib/storage/createWebStorage"; import { createAsyncMigrate } from "./createAsyncMigrate"; import { VERSION, accountsMigrations, mainStoreMigrations } from "./migrations"; +import { wcSlice } from "./slices"; import { accountsSlice } from "./slices/accounts/accounts"; import { announcementSlice } from "./slices/announcement"; import { assetsSlice } from "./slices/assets"; @@ -56,6 +57,7 @@ export const makeReducer = (storage_: Storage | undefined) => { assets: assetsSlice.reducer, batches: batchesSlice.reducer, beacon: beaconSlice.reducer, + walletconnect: wcSlice.reducer, contacts: contactsSlice.reducer, errors: errorsSlice.reducer, multisigs: multisigsSlice.reducer, diff --git a/packages/state/src/slices/WalletConnect.ts b/packages/state/src/slices/WalletConnect.ts new file mode 100644 index 0000000000..7d80d9c3af --- /dev/null +++ b/packages/state/src/slices/WalletConnect.ts @@ -0,0 +1,29 @@ +import { createSlice } from "@reduxjs/toolkit"; + +export type WalletConnectInfo = { + peerListUpdatedToggle: boolean; +}; + +// mapping topic -> connection info +export type State = WalletConnectInfo; + +export const wcInitialState: State = { peerListUpdatedToggle: false }; + +/** + * Stores connection info between dApps and accounts. + * + * dApps are identified by topic (a unique string id generated from dApp public key). + */ +export const wcSlice = createSlice({ + name: "walletconnect", + initialState: wcInitialState, + reducers: { + reset: () => wcInitialState, + + togglePeerListUpdated: state => { + state.peerListUpdatedToggle = !state.peerListUpdatedToggle; + }, + }, +}); + +export const wcActions = wcSlice.actions; diff --git a/packages/state/src/slices/beacon.ts b/packages/state/src/slices/beacon.ts index 0e4216fdd2..1ef38049e8 100644 --- a/packages/state/src/slices/beacon.ts +++ b/packages/state/src/slices/beacon.ts @@ -3,12 +3,12 @@ import { createSlice } from "@reduxjs/toolkit"; import { type RawPkh } from "@umami/tezos"; import { fromPairs } from "lodash"; -export type DAppConnectionInfo = { +export type DAppBeaconConnectionInfo = { accountPkh: RawPkh; networkType: NetworkType; }; -type State = Record; +type State = Record; export const beaconInitialState: State = {}; diff --git a/packages/state/src/slices/index.ts b/packages/state/src/slices/index.ts index e3346a7311..b2a674b959 100644 --- a/packages/state/src/slices/index.ts +++ b/packages/state/src/slices/index.ts @@ -9,3 +9,4 @@ export * from "./multisigs"; export * from "./networks"; export * from "./tokens"; export * from "./protocolSettings"; +export * from "./WalletConnect"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ccb4a9d67..be9eb78a43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1800,9 +1800,12 @@ importers: '@walletconnect/jsonrpc-utils': specifier: ^1.0.8 version: 1.0.8 - '@walletconnect/utils': + '@walletconnect/types': specifier: ^2.16.2 version: 2.17.2 + '@walletconnect/utils': + specifier: ^2.16.2 + version: 2.16.2 bip39: specifier: ^3.1.0 version: 3.1.0 From 2427dc24048ba07f697fbf33f6182ab161b9c0aa Mon Sep 17 00:00:00 2001 From: Diana Savatina Date: Tue, 3 Dec 2024 13:59:53 +0000 Subject: [PATCH 2/2] fix: mocks for getActiveSessions --- .../Menu/AppsMenu/AppsMenu.test.tsx | 52 +++++++++++++++++-- apps/web/src/components/Menu/Menu.test.tsx | 13 +++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/Menu/AppsMenu/AppsMenu.test.tsx b/apps/web/src/components/Menu/AppsMenu/AppsMenu.test.tsx index 2bc727db03..2e8446c4e2 100644 --- a/apps/web/src/components/Menu/AppsMenu/AppsMenu.test.tsx +++ b/apps/web/src/components/Menu/AppsMenu/AppsMenu.test.tsx @@ -1,15 +1,40 @@ -import { WalletClient } from "@umami/state"; +jest.mock("@umami/state", () => ({ + ...jest.requireActual("@umami/state"), + walletKit: { + core: {}, + metadata: { + name: "AppMenu test", + description: "Umami Wallet with WalletConnect", + url: "https://umamiwallet.com", + icons: ["https://umamiwallet.com/assets/favicon-32-45gq0g6M.png"], + }, + getActiveSessions: jest.fn(), + pair: jest.fn(), + }, + createWalletKit: jest.fn(), +})); + +import { WalletClient, walletKit } from "@umami/state"; import { AppsMenu } from "./AppsMenu"; import { act, renderInDrawer, screen, userEvent } from "../../../testUtils"; describe("", () => { - it("calls addPeer on button click with the copied text", async () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("calls addPeer for Beacon on button click with the copied text", async () => { const user = userEvent.setup(); const payload = "btunoo2sZmmMB6k9Bef8tgYs7PsS6g6DFdUiDzVuwMxv7nGJN71eFCtGxGfq321pFy4eT2ckDWWzTdBhvje7VUzy2ZciQSe9rGMCF6Fpx5MCM3q2CWyUt4nhqSFigPhcUHaLAzAwcSTXbSRn9YZ8QJwwaWzdsNF6UW4PrWeCbABvHArBDpeLRNxJRjMpAVndoCCf9Vbu7YRXF2FcxWxUrcqfj1i3hr34M8zRTtP5QuVqita8MW5A6Ub3tB3bDvykqa8aYFvxbWr47USytTQjVqnnFUdBo8rm3cJyUq39hJwUdbvZEyoGUWnfuhFHYcbyZP86CPef1p7Eh1KUEwVKxLxQwNX84Eg1eBkZowRtNKcqqShMhKT7ZEELyfh1ji7NckRF8RJuwuco4dqBg6msuZjZqta4CsJvQw4A66RbePC8LxwKEb3Nhha8cygtbQVC4Scb7PaLY9qwQJjYL7n"; jest.spyOn(navigator.clipboard, "readText").mockResolvedValue(payload); - const mockAddPeer = jest.spyOn(WalletClient, "addPeer"); + jest.spyOn(walletKit, "getActiveSessions").mockImplementation(() => ({})); + + // make sure the mocks are correct + expect(walletKit.metadata.name).toEqual("AppMenu test"); + expect(walletKit.getActiveSessions()).toEqual({}); + + const mockAddPeer = jest.spyOn(WalletClient, "addPeer").mockResolvedValue(undefined); await renderInDrawer(); @@ -25,4 +50,25 @@ describe("", () => { version: "3", }); }); + + it("handles WalletConenct request on button click with the copied text", async () => { + const user = userEvent.setup(); + const payload = + "wc:c02d87d6f8c46a9192e1fd4627b5104d326ee6ec4dd9040482a277bdc53e2f10@2?expiryTimestamp=1733241891&relay-protocol=irn&symKey=d8b5f7b8a35b7e73126bfe4af89568811a87c4cfd49e3946c44026d55267ebd7"; + jest.spyOn(navigator.clipboard, "readText").mockResolvedValue(payload); + jest.spyOn(walletKit, "getActiveSessions").mockImplementation(() => ({})); + + // make sure the mocks are correct + expect(walletKit.metadata.name).toEqual("AppMenu test"); + expect(walletKit.getActiveSessions()).toEqual({}); + + const mockAddPeer = jest.spyOn(WalletClient, "addPeer").mockResolvedValue(undefined); + + await renderInDrawer(); + + await act(() => user.click(screen.getByText("Connect"))); + + expect(mockAddPeer).not.toHaveBeenCalled(); + expect(walletKit.pair).toHaveBeenCalledWith({ uri: payload }); + }); }); diff --git a/apps/web/src/components/Menu/Menu.test.tsx b/apps/web/src/components/Menu/Menu.test.tsx index b9830bd181..492eacd47a 100644 --- a/apps/web/src/components/Menu/Menu.test.tsx +++ b/apps/web/src/components/Menu/Menu.test.tsx @@ -6,6 +6,7 @@ import { addTestAccount, makeStore, useDownloadBackupFile, + walletKit, } from "@umami/state"; import { AddressBookMenu } from "./AddressBookMenu/AddressBookMenu"; @@ -30,6 +31,17 @@ jest.mock("@chakra-ui/system", () => ({ jest.mock("@umami/state", () => ({ ...jest.requireActual("@umami/state"), useDownloadBackupFile: jest.fn(), + walletKit: { + core: {}, + metadata: { + name: "AppMenu test", + description: "Umami Wallet with WalletConnect", + url: "https://umamiwallet.com", + icons: ["https://umamiwallet.com/assets/favicon-32-45gq0g6M.png"], + }, + getActiveSessions: jest.fn(), + }, + createWalletKit: jest.fn(), })); let store: UmamiStore; @@ -71,6 +83,7 @@ describe("", () => { ])("opens %label menu correctly", async (label, Component) => { const user = userEvent.setup(); const { openWith } = dynamicDrawerContextMock; + jest.spyOn(walletKit, "getActiveSessions").mockImplementation(() => ({})); await renderInDrawer(, store);