From a18d63b238ba762002bb94296b84b5bce8a48f2d Mon Sep 17 00:00:00 2001 From: Diana Savatina Date: Tue, 1 Oct 2024 14:03:39 +0100 Subject: [PATCH] feat: WalletConnect integration, part 2, pairing list --- apps/desktop/src/Router.tsx | 13 +- 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 | 13 +- .../WalletConnect/WalletConnectPeers.tsx | 128 ++++++++++++++++++ .../WalletConnect/WalletConnectProvider.tsx | 16 ++- .../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 | 86 ++++++++++++ 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 | 46 +++++++ packages/state/src/slices/beacon.ts | 4 +- packages/state/src/slices/index.ts | 1 + pnpm-lock.yaml | 5 +- 22 files changed, 352 insertions(+), 53 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..c82bd67eb9 100644 --- a/apps/desktop/src/Router.tsx +++ b/apps/desktop/src/Router.tsx @@ -1,7 +1,12 @@ /* 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, + useResetWcConnections, +} from "@umami/state"; import { noop } from "lodash"; import { useEffect } from "react"; import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; @@ -59,11 +64,13 @@ const LoggedInRouterWithPolling = () => { }; const LoggedOutRouter = () => { - const resetBeaconConnections = useResetConnections(); + const resetBeaconConnections = useResetBeaconConnections(); + const resetWcConnections = useResetWcConnections(); useEffect(() => { WalletClient.destroy().then(resetBeaconConnections).catch(noop); - }, [resetBeaconConnections]); + resetWcConnections(); + }, [resetBeaconConnections, resetWcConnections]); return ( 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 8735e66f68..7f973c305c 100644 --- a/apps/desktop/src/utils/beacon/PermissionRequestModal.tsx +++ b/apps/desktop/src/utils/beacon/PermissionRequestModal.tsx @@ -26,7 +26,7 @@ import { import { useDynamicModalContext } from "@umami/components"; import { WalletClient, - useAddConnection, + useAddBeaconConnection, useAsyncActionHandler, useGetImplicitAccount, } from "@umami/state"; @@ -38,7 +38,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 834f9fe441..f95d58e937 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 e409a2404e..20f95f6938 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, Divider, 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 = () => { @@ -31,6 +31,7 @@ export const AppsMenu = () => { + ); }; diff --git a/apps/web/src/components/WalletConnect/SessionProposalModal.tsx b/apps/web/src/components/WalletConnect/SessionProposalModal.tsx index 375a88411f..08aecd663e 100644 --- a/apps/web/src/components/WalletConnect/SessionProposalModal.tsx +++ b/apps/web/src/components/WalletConnect/SessionProposalModal.tsx @@ -16,7 +16,14 @@ import { } from "@chakra-ui/react"; import { type WalletKitTypes } from "@reown/walletkit"; import { useDynamicModalContext } from "@umami/components"; -import { useAsyncActionHandler, useGetImplicitAccount, walletKit } from "@umami/state"; +import { + useAddWcConnection, + useAsyncActionHandler, + useGetImplicitAccount, + walletKit, +} from "@umami/state"; +import { type NetworkName } from "@umami/tezos"; +import { type SessionTypes } from "@walletconnect/types"; import { buildApprovedNamespaces, getSdkError } from "@walletconnect/utils"; import { FormProvider, useForm } from "react-hook-form"; @@ -32,6 +39,7 @@ export const SessionProposalModal = ({ proposal: WalletKitTypes.SessionProposal; network: NetworkType; }) => { + const addConnectionToWcSlice = useAddWcConnection(); const getAccount = useGetImplicitAccount(); const { onClose } = useDynamicModalContext(); @@ -62,11 +70,12 @@ export const SessionProposalModal = ({ }, }); - await walletKit.approveSession({ + const session: SessionTypes.Struct = await walletKit.approveSession({ id: proposal.id, namespaces, sessionProperties: {}, }); + addConnectionToWcSlice(session, account.address.pkh, network.split(":")[1] as NetworkName); 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..1e8e6e18f4 --- /dev/null +++ b/apps/web/src/components/WalletConnect/WalletConnectPeers.tsx @@ -0,0 +1,128 @@ +import { Center, Divider, Flex, Heading, IconButton, Image, Text, VStack } from "@chakra-ui/react"; +import { useGetWcConnectionInfo, useRemoveWcPeer, useWcPeers } 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 { CodeSandboxIcon, StubIcon as TrashIcon } from "../../assets/icons"; +import { useColor } from "../../styles/useColor"; +import { AddressPill } from "../AddressPill/AddressPill"; +import { EmptyMessage } from "../EmptyMessage"; + +/** + * Component displaying a list of connected dApps. + * + * Loads dApps data from {@link useWcPeers} hook & zips it with generated dAppIds. + */ +export const WcPeers = () => { + // const wcPeers: Record = walletKit.getActiveSessions(); + const { peers: wcPeers } = useWcPeers(); + + console.log("wcPeers", wcPeers); + + if (Object.keys(wcPeers).length === 0) { + return ( + + ); + } + + return ( + } + spacing="0" + > + { + // loop peers and print PeerRow + Object.entries(wcPeers).map(([topic, peerInfo]) => ( + + )) + } + + ); +}; + +/** + * Component for displaying info about single connected dApp. + * + * @param peerInfo - peerInfo provided by wc Api + computed dAppId. + * @param onRemove - action for deleting dApp connection. + */ +const PeerRow = ({ peerInfo }: { peerInfo: SessionTypes.Struct }) => { + const color = useColor(); + const removeWcPeer = useRemoveWcPeer(); + + return ( +
+ +
+ } + src={peerInfo.peer.metadata.icons[0]} + /> +
+
+ + {peerInfo.peer.metadata.name} + + +
+
+ } + onClick={() => + removeWcPeer({ topic: peerInfo.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, + * if information about the connection is stored in {@link wcSlice}. + * + * @param peerInfo - peerInfo provided by wc Api + computed dAppId. + */ +const StoredPeerInfo = ({ peerInfo }: { peerInfo: SessionTypes.Struct }) => { + const connectionInfo = useGetWcConnectionInfo(peerInfo.topic); + + if (!connectionInfo) { + return null; + } + + return ( + + + + + Network: + + + {capitalize(connectionInfo.networkName)} + + + ); +}; diff --git a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx index 97708dd834..d19d4e0c03 100644 --- a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx +++ b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx @@ -6,6 +6,8 @@ import { createWalletKit, useAsyncActionHandler, useAvailableNetworks, + useRemoveWcConnection, + useWcPeers, walletKit, } from "@umami/state"; import { type Network } from "@umami/tezos"; @@ -89,12 +91,20 @@ const useOnSessionRequest = () => { // dApp can release WalletConnect session at any time and then the Wallet is notified by the WalletConnect server. const useOnSessionDelete = () => { + const { handleAsyncAction } = useAsyncActionHandler(); + const { peers, refresh } = useWcPeers(); + const removeWcPeer = useRemoveWcConnection(); const toast = useToast(); return (event: WalletKitTypes.SessionDelete) => - toast({ - description: `dApp ${event.topic} released the connection.`, - status: "info", + handleAsyncAction(async () => { + const { topic } = event; + toast({ + description: `Session deleted by dApp ${peers[topic].peer.metadata.name}`, + status: "info", + }); + removeWcPeer(topic); + await refresh(); }); }; 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 90f1e6fc25..51fbaa6d8d 100644 --- a/apps/web/src/components/beacon/PermissionRequestModal.tsx +++ b/apps/web/src/components/beacon/PermissionRequestModal.tsx @@ -25,7 +25,7 @@ import { import { useDynamicModalContext } from "@umami/components"; import { WalletClient, - useAddConnection, + useAddBeaconConnection, useAsyncActionHandler, useGetImplicitAccount, } from "@umami/state"; @@ -40,7 +40,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 834f9fe441..402ba662b7 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 d78817f3e3..e7545f7820 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", "axios": "^1.7.7", "bip39": "^3.1.0", diff --git a/packages/state/src/hooks/WalletConnect.ts b/packages/state/src/hooks/WalletConnect.ts new file mode 100644 index 0000000000..c850d6384a --- /dev/null +++ b/packages/state/src/hooks/WalletConnect.ts @@ -0,0 +1,86 @@ +import { useQuery } from "@tanstack/react-query"; +import { type NetworkName, type RawPkh } from "@umami/tezos"; +import { type ErrorResponse } from "@walletconnect/jsonrpc-utils"; +import { type SessionTypes } from "@walletconnect/types"; +import { uniq } from "lodash"; +import { useDispatch } from "react-redux"; + +import { useAppSelector } from "./useAppSelector"; +import { type DAppWcConnectionInfo, wcActions } from "../slices"; +import { walletKit } from "../walletConnect"; + +/** + * Returns connected account pkh & network by a given topic. + * + * @param topic - generated from dApp public key. + */ +export const useGetWcConnectionInfo = (topic: string): DAppWcConnectionInfo | undefined => { + const connections = useAppSelector(s => s.walletconnect); + return connections[topic]; +}; + +export const useGetWcPeersForAccounts = () => { + const connections = useAppSelector(s => s.walletconnect); + + return (pkhs: RawPkh[]) => + uniq( + Object.entries(connections) + .filter(([_, connectionInfo]) => pkhs.includes(connectionInfo.accountPkh)) + .map(([topic, _]) => topic) + ); +}; + +/** + * Returns function for removing all connections from {@link wcSlice}. + */ +export const useResetWcConnections = () => { + const dispatch = useDispatch(); + return () => dispatch(wcActions.reset()); +}; + +/** + * Returns function for adding connection info to {@link wcSlice}. + */ +export const useAddWcConnection = () => { + const { refresh } = useWcPeers(); + const dispatch = useDispatch(); + return (session: SessionTypes.Struct, accountPkh: RawPkh, chain: NetworkName) => { + dispatch(wcActions.addConnection({ topic: session.topic, accountPkh, networkName: chain })); + void refresh(); + }; +}; + +/** + * Returns function for removing connection from {@link wcSlice}. + */ +export const useRemoveWcConnection = () => { + const dispatch = useDispatch(); + return (topic: string) => dispatch(wcActions.removeConnection(topic)); +}; + +export const useWcPeers = () => { + const query = useQuery>({ + queryKey: ["wcPeers"], + queryFn: async () => { + const wcPeers: Record = walletKit.getActiveSessions(); + console.log("Active WC sessions:", wcPeers); + return wcPeers; + }, + initialData: {}, + }); + + return { peers: query.data, refresh: query.refetch }; +}; + +export const useRemoveWcPeer = () => { + const { refresh } = useWcPeers(); + const removeConnectionFromWcSlice = useRemoveWcConnection(); + + return (params: { topic: string; reason: ErrorResponse }) => { + console.log("disconnecting WC session", params); + walletKit + .disconnectSession(params) + .then(() => removeConnectionFromWcSlice(params.topic)) + .finally(() => void refresh()); + }; +}; 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 c1e376bcf4..ffe888543d 100644 --- a/packages/state/src/reducer.ts +++ b/packages/state/src/reducer.ts @@ -4,6 +4,7 @@ import storage from "redux-persist/lib/storage"; 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"; @@ -38,6 +39,7 @@ const rootReducers = combineReducers({ 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..ebb08dde2e --- /dev/null +++ b/packages/state/src/slices/WalletConnect.ts @@ -0,0 +1,46 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { type NetworkName, type RawPkh } from "@umami/tezos"; +import { fromPairs } from "lodash"; + +export type DAppWcConnectionInfo = { + accountPkh: RawPkh; + networkName: NetworkName; +}; + +// mapping topic -> connection info +type State = Record; + +export const wcInitialState: State = {}; + +/** + * 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, + + addConnection: ( + state, + { payload }: { payload: { topic: string; accountPkh: RawPkh; networkName: NetworkName } } + ) => { + state[payload.topic] = { accountPkh: payload.accountPkh, networkName: payload.networkName }; + }, + + removeConnection: (state, { payload: topic }: { payload: string }) => { + delete state[topic]; + }, + + removeConnections: (state, { payload: pkhs }: { payload: RawPkh[] }) => + fromPairs( + Object.entries(state).filter( + ([_, connectionInfo]) => !pkhs.includes(connectionInfo.accountPkh) + ) + ), + }, +}); + +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 88be76029c..ab2199ea8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1585,6 +1585,9 @@ importers: '@walletconnect/jsonrpc-utils': specifier: ^1.0.8 version: 1.0.8 + '@walletconnect/types': + specifier: ^2.16.2 + version: 2.16.2 '@walletconnect/utils': specifier: ^2.16.2 version: 2.16.2 @@ -1675,7 +1678,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.1.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.1.0)(typescript@5.5.4)) + version: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.4)) madge: specifier: ^8.0.0 version: 8.0.0(typescript@5.5.4)