Skip to content

Commit

Permalink
feat: WalletConnect integration, part 1, session proposal
Browse files Browse the repository at this point in the history
This is the first part of the WalletConnect integration. It includes the following components:
 - initiating WalletKit by WalletConnect
 - subscribing to basic events
 - handling session proposal on a basic level

Limitations:
 - all requests are rejected
 - no pairing list
 - no way to disconnect
 - no verification of dapp, no check for scam
  • Loading branch information
dianasavvatina committed Oct 1, 2024
1 parent 82b2cd2 commit 021d090
Show file tree
Hide file tree
Showing 18 changed files with 1,050 additions and 23 deletions.
4 changes: 4 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@reduxjs/toolkit": "^2.2.7",
"@reown/walletkit": "^1.0.1",
"@tanstack/react-query": "^5.56.2",
"@taquito/beacon-wallet": "^20.0.1",
"@taquito/ledger-signer": "^20.0.1",
Expand All @@ -49,6 +50,9 @@
"@umami/state": "workspace:^",
"@umami/tezos": "workspace:^",
"@umami/tzkt": "workspace:^",
"@walletconnect/jsonrpc-utils": "^1.0.8",
"@walletconnect/types": "^2.16.2",
"@walletconnect/utils": "^2.16.2",
"bignumber.js": "^9.1.2",
"bip39": "^3.1.0",
"cross-env": "^7.0.3",
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { useCurrentAccount } from "@umami/state";
import { Layout } from "../../Layout";
import { Welcome } from "../../views/Welcome";
import { BeaconProvider } from "../beacon";
import { WalletConnectProvider } from "../WalletConnect/WcProvider";

export const App = () => {
const currentAccount = useCurrentAccount();

return currentAccount ? (
<BeaconProvider>
<Layout />
<WalletConnectProvider>
<Layout />
</WalletConnectProvider>
</BeaconProvider>
) : (
<Welcome />
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/components/Menu/AppsMenu/AppsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Button, Divider, Text } from "@chakra-ui/react";
import { useAddPeer } from "@umami/state";

import { BeaconPeers } from "../../beacon";
import { onConnect } from "../../WalletConnect";
import { DrawerContentWrapper } from "../DrawerContentWrapper";

export const AppsMenu = () => {
Expand All @@ -10,13 +11,17 @@ export const AppsMenu = () => {
return (
<DrawerContentWrapper title="Apps">
<Text marginTop="12px" size="lg">
Connect with Pairing Request
Connect with Pairing Request for Beacon or WalletConnect
</Text>
<Button
width="fit-content"
marginTop="18px"
padding="0 24px"
onClick={() => navigator.clipboard.readText().then(addPeer)}
onClick={() =>
navigator.clipboard
.readText()
.then(payload => (payload.startsWith("wc:") ? onConnect(payload) : addPeer(payload)))
}
variant="secondary"
>
Connect
Expand Down
60 changes: 60 additions & 0 deletions apps/web/src/components/WalletConnect/ModalFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Box, Button, Card, HStack } from "@chakra-ui/react";
import { useFormContext } from "react-hook-form";
export interface LoaderProps {
color?: "default" | "primary" | "secondary" | "success" | "warning" | "error" | "white";
active?: boolean;
}
interface Props {
onApprove: () => void;
onReject: () => void;
infoBoxCondition?: boolean;
infoBoxText?: string;
approveLoader?: LoaderProps;
rejectLoader?: LoaderProps;
disableApprove?: boolean;
disableReject?: boolean;
}

export default function ModalFooter({
onApprove,
approveLoader,
onReject,
rejectLoader,
infoBoxCondition,
infoBoxText,
disableApprove,
disableReject,
}: Props) {
const form = useFormContext();
return (
<Box as="footer" padding={4}>
{infoBoxCondition && (
<Box marginBottom={4} textAlign="left">
<Card marginLeft={2}>{infoBoxText || ""}</Card>
</Box>
)}
<HStack alignItems="center" justifyContent="space-between">
<Button
colorScheme="gray"
data-testid="session-reject-button"
isDisabled={disableReject || rejectLoader?.active}
onClick={onReject}
variant="solid"
>
{rejectLoader && rejectLoader.active ? "Loading..." : "Reject"}
</Button>
<Button
colorScheme="blue"
data-testid="session-approve-button"
isDisabled={
disableApprove || approveLoader?.active || Boolean(form.formState.errors.password)
}
onClick={form.handleSubmit(onApprove)}
variant="solid"
>
{approveLoader && approveLoader.active ? "Loading..." : "Approve"}
</Button>
</HStack>
</Box>
);
}
48 changes: 48 additions & 0 deletions apps/web/src/components/WalletConnect/PairingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Box, Button, Card, CardBody, Link, Text, Tooltip } from "@chakra-ui/react";
import { truncate } from "@umami/tezos";

import { CrossedCircleIcon } from "../../assets/icons"; // Ensure this path is correct

interface IProps {
name?: string;
url?: string;
topic?: string;
onDelete: () => Promise<void>;
}

export default function PairingCard({ topic, name, url, onDelete }: IProps) {
return (
<Card position="relative" minHeight="70px" marginBottom={6} borderWidth={1} borderColor="light">
<CardBody
alignItems="center"
justifyContent="space-between"
flexDirection="row"
display="flex"
overflow="hidden"
padding={4}
>
<Box flex="1">
<Text marginLeft={9} data-testid={`pairing-text-${topic}`}>
{name}
</Text>
<Link marginLeft={9} data-testid={`pairing-text-${topic}`} href={url}>
{truncate(url?.split("https://")[1] ?? "Unknown", 23)}
</Link>
</Box>
<Tooltip label="Delete" placement="left">
<Button
minWidth="auto"
padding={1}
_hover={{ bg: "red.100", transition: "all 0.2s" }}
colorScheme="red"
data-testid={`pairing-delete-${topic}`}
onClick={onDelete}
variant="outline"
>
<CrossedCircleIcon alt="delete icon" />
</Button>
</Tooltip>
</CardBody>
</Card>
);
}
48 changes: 48 additions & 0 deletions apps/web/src/components/WalletConnect/ProjectInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Avatar, Box, Card, Flex, Heading, Link, Text } from "@chakra-ui/react";
import { type SignClientTypes } from "@walletconnect/types";

import { PencilIcon } from "../../assets/icons";

interface IProps {
metadata: SignClientTypes.Metadata;
intention?: string;
}

/**
* dApp project info card. Contains verification info to help user decide if the dApp is safe to connect.
*/
export default function ProjectInfoCard({ metadata, intention }: IProps) {
const { icons, name, url } = metadata;

return (
<Box textAlign="center">
<Box>
<Avatar marginX="auto" size="lg" src={icons[0]} />
</Box>
<Box marginTop={4}>
<Text data-testid="session-info-card-text">
<Text as="span" fontWeight="bold">
{name}
</Text>{" "}
<br />
<Heading size="md">wants to {intention ? intention : "connect"}</Heading>
</Text>
</Box>
<Box marginTop={4}>
<Link
verticalAlign="middle"
marginLeft={2}
data-testid="session-info-card-url"
href={url}
isExternal
>
{url}
</Link>
</Box>
<Flex alignItems="center" justifyContent="center" marginTop={4}>
<PencilIcon style={{ verticalAlign: "bottom" }} />
<Card marginLeft={2}>Cannot Verify: to be implemented</Card>
</Flex>
</Box>
);
}
73 changes: 73 additions & 0 deletions apps/web/src/components/WalletConnect/RequestModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Divider } from "@chakra-ui/react";
import { type CoreTypes } from "@walletconnect/types";
import { Fragment, type ReactNode, useMemo } from "react";

import ModalFooter, { type LoaderProps } from "./ModalFooter";
import ProjectInfoCard from "./ProjectInfoCard";
import RequestModalContainer from "./RequestModalContainer";
import VerifyInfobox from "./VerifyInfobox";

interface IProps {
children: ReactNode;
metadata: CoreTypes.Metadata;
onApprove: () => void;
onReject: () => void;
intention?: string;
infoBoxCondition?: boolean;
infoBoxText?: string;
approveLoader?: LoaderProps;
rejectLoader?: LoaderProps;
disableApprove?: boolean;
disableReject?: boolean;
}
export default function RequestModal({
children,
metadata,
onApprove,
onReject,
approveLoader,
rejectLoader,
intention,
infoBoxCondition,
infoBoxText,
disableApprove,
disableReject,
}: IProps) {
const modalContent = useMemo(
() => (
<>
<RequestModalContainer title="">
<ProjectInfoCard intention={intention} metadata={metadata} />
<Divider />
{children}
<Divider />
<VerifyInfobox />
</RequestModalContainer>
<ModalFooter
approveLoader={approveLoader}
disableApprove={disableApprove}
disableReject={disableReject}
infoBoxCondition={infoBoxCondition}
infoBoxText={infoBoxText}
onApprove={onApprove}
onReject={onReject}
rejectLoader={rejectLoader}
/>
</>
),
[
approveLoader,
children,
infoBoxCondition,
infoBoxText,
intention,
metadata,
onApprove,
onReject,
rejectLoader,
disableApprove,
disableReject,
]
);
return <Fragment>{modalContent}</Fragment>;
}
28 changes: 28 additions & 0 deletions apps/web/src/components/WalletConnect/RequestModalContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Card, Heading, ModalBody, ModalHeader } from "@chakra-ui/react";
import { Fragment, type ReactNode } from "react";

/**
* Types
*/
interface IProps {
title?: string;
children: ReactNode | ReactNode[];
}

/**
* Component
*/
export default function RequestModalContainer({ children, title }: IProps) {
return (
<Fragment>
{title ? (
<ModalHeader>
<Heading size="sm">{title}</Heading>
</ModalHeader>
) : null}
<ModalBody>
<Card>{children}</Card>
</ModalBody>
</Fragment>
);
}
Loading

0 comments on commit 021d090

Please sign in to comment.