Skip to content

Commit

Permalink
Merge pull request #1985 from trilitech/add-wc-slice
Browse files Browse the repository at this point in the history
feat: Adding WalletConnect data to peer list and slices
  • Loading branch information
dianasavvatina authored Dec 4, 2024
2 parents 4ae8d38 + 2427dc2 commit 6a28b2c
Show file tree
Hide file tree
Showing 25 changed files with 334 additions and 68 deletions.
4 changes: 2 additions & 2 deletions apps/desktop/src/Router.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -59,7 +59,7 @@ const LoggedInRouterWithPolling = () => {
};

const LoggedOutRouter = () => {
const resetBeaconConnections = useResetConnections();
const resetBeaconConnections = useResetBeaconConnections();

useEffect(() => {
WalletClient.destroy().then(resetBeaconConnections).catch(noop);
Expand Down
12 changes: 6 additions & 6 deletions apps/desktop/src/utils/beacon/BeaconPeers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 (
Expand Down Expand Up @@ -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 (
<Flex justifyContent="space-between" height="106px" data-testid="peer-row" paddingY="30px">
Expand All @@ -76,7 +76,7 @@ const PeerRow = ({ peerInfo }: { peerInfo: ExtendedPeerInfo }) => {
<IconButton
aria-label="Remove Peer"
icon={<TrashIcon />}
onClick={() => removePeer(peerInfo)}
onClick={() => removeBeaconPeer(peerInfo)}
size="xs"
variant="circle"
/>
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/utils/beacon/PermissionRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { useDynamicModalContext } from "@umami/components";
import {
WalletClient,
useAddConnection,
useAddBeaconConnection,
useAsyncActionHandler,
useGetImplicitAccount,
} from "@umami/state";
Expand All @@ -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();
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useAsyncActionHandler,
useFindNetwork,
useGetOwnedAccountSafe,
useRemovePeerBySenderId,
useRemoveBeaconPeerBySenderId,
} from "@umami/state";
import { type Network } from "@umami/tezos";

Expand All @@ -23,15 +23,15 @@ 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 = () => {
const { openWith } = useDynamicModalContext();
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 = ({
Expand Down
52 changes: 49 additions & 3 deletions apps/web/src/components/Menu/AppsMenu/AppsMenu.test.tsx
Original file line number Diff line number Diff line change
@@ -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("<AppsMenu />", () => {
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(<AppsMenu />);

Expand All @@ -25,4 +50,25 @@ describe("<AppsMenu />", () => {
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(<AppsMenu />);

await act(() => user.click(screen.getByText("Connect")));

expect(mockAddPeer).not.toHaveBeenCalled();
expect(walletKit.pair).toHaveBeenCalledWith({ uri: payload });
});
});
3 changes: 2 additions & 1 deletion apps/web/src/components/Menu/AppsMenu/AppsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -36,6 +36,7 @@ export const AppsMenu = () => {
title="Apps"
>
<BeaconPeers />
<WcPeers />
</DrawerContentWrapper>
);
};
13 changes: 13 additions & 0 deletions apps/web/src/components/Menu/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
addTestAccount,
makeStore,
useDownloadBackupFile,
walletKit,
} from "@umami/state";

import { AddressBookMenu } from "./AddressBookMenu/AddressBookMenu";
Expand All @@ -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;
Expand Down Expand Up @@ -71,6 +83,7 @@ describe("<Menu />", () => {
])("opens %label menu correctly", async (label, Component) => {
const user = userEvent.setup();
const { openWith } = dynamicDrawerContextMock;
jest.spyOn(walletKit, "getActiveSessions").mockImplementation(() => ({}));

await renderInDrawer(<Menu />, store);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -35,6 +40,7 @@ export const SessionProposalModal = ({
network: NetworkType;
}) => {
const getAccount = useGetImplicitAccount();
const toggleWcPeerListUpdated = useToggleWcPeerListUpdated();
const color = useColor();

const { onClose } = useDynamicModalContext();
Expand Down Expand Up @@ -71,6 +77,7 @@ export const SessionProposalModal = ({
sessionProperties: {},
});
console.log("WC session approved", session);
toggleWcPeerListUpdated();
onClose();
});

Expand Down
131 changes: 131 additions & 0 deletions apps/web/src/components/WalletConnect/WalletConnectPeers.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, SessionTypes.Struct>>({});
const isUpdated = useGetWcPeerListToggle();

useEffect(() => {
const sessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
setSessions(sessions);
}, [isUpdated]);

if (!Object.keys(sessions).length) {
return (
<EmptyMessage
alignItems="flex-start"
marginTop="40px"
data-testid="wc-peers-empty"
subtitle="No WalletConnect Apps to show"
title="Your WalletConnect Apps will appear here..."
/>
);
}

return (
<VStack
alignItems="flex-start"
gap="24px"
marginTop="24px"
data-testid="wc-peers"
divider={<Divider />}
spacing="0"
>
{
// loop peers and print PeerRow
Object.entries(sessions).map(([topic, sessionInfo]) => (
<PeerRow key={topic} sessionInfo={sessionInfo} />
))
}
</VStack>
);
};

/**
* 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 (
<Center
alignItems="center"
justifyContent="space-between"
width="full"
height="60px"
data-testid="peer-row"
>
<Flex height="100%">
<Center width="60px" marginRight="12px">
<Image
objectFit="cover"
fallback={<CodeSandboxIcon width="36px" height="36px" />}
src={sessionInfo.peer.metadata.icons[0]}
/>
</Center>
<Center alignItems="flex-start" flexDirection="column" gap="6px">
<Text color={color("900")} size="lg">
{sessionInfo.peer.metadata.name}
</Text>
<StoredSessionInfo sessionInfo={sessionInfo} />
</Center>
</Flex>
<IconButton
color={color("500")}
aria-label="Remove Peer"
icon={<TrashIcon />}
onClick={() =>
disconnectWalletConnectPeer({
topic: sessionInfo.topic,
reason: getSdkError("USER_DISCONNECTED"),
})
}
variant="iconButtonSolid"
/>
</Center>
);
};

/**
* 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 }) => (
<Flex>
<AddressPill
marginRight="10px"
address={parsePkh(sessionInfo.namespaces.tezos.accounts[0].split(":")[2])}
/>
<Divider marginRight="10px" orientation="vertical" />
<Text marginTop="2px" marginRight="4px" fontWeight={600} size="sm">
Network:
</Text>
<Text marginTop="2px" data-testid="dapp-connection-network" size="sm">
{capitalize(sessionInfo.namespaces.tezos.chains?.[0].split(":")[1] ?? "")}
</Text>
</Flex>
);
Loading

1 comment on commit 6a28b2c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 84%
83.88% (1764/2103) 79.35% (838/1056) 78.45% (448/571)
apps/web Coverage: 84%
83.88% (1764/2103) 79.35% (838/1056) 78.45% (448/571)
packages/components Coverage: 97%
97.51% (196/201) 95.91% (94/98) 88.13% (52/59)
packages/core Coverage: 81%
81.91% (222/271) 71.22% (99/139) 81.96% (50/61)
packages/crypto Coverage: 100%
100% (43/43) 90.9% (10/11) 100% (7/7)
packages/data-polling Coverage: 97%
95.27% (141/148) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (36/36)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 85%
84.76% (818/965) 80.86% (186/230) 78.59% (301/383)
packages/tezos Coverage: 89%
88.72% (118/133) 94.59% (35/37) 86.84% (33/38)
packages/tzkt Coverage: 89%
87.32% (62/71) 87.5% (14/16) 80.48% (33/41)

Please sign in to comment.