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 4, 2024
1 parent 82b2cd2 commit ffcbede
Show file tree
Hide file tree
Showing 13 changed files with 792 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/WalletConnectProvider";

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
47 changes: 47 additions & 0 deletions apps/web/src/components/WalletConnect/ProjectInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Avatar, Box, Card, Flex, Heading, Icon, Link, Text } from "@chakra-ui/react";
import { type SignClientTypes } from "@walletconnect/types";

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

type Props = {
metadata: SignClientTypes.Metadata;
intention?: string;
};

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

return (
<Box textAlign="center">
<Box>
<Avatar marginX="auto" size="lg" src={icons[0]} />
</Box>
<Box marginTop="16px">
<Text data-testid="session-info-card-text">
<Text as="span" fontWeight="bold">
{name}
</Text>{" "}
<Heading size="md">wants to {intention ?? "connect"}</Heading>
</Text>
</Box>
<Box marginTop="16px">
<Link
verticalAlign="middle"
marginLeft="8px"
data-testid="session-info-card-url"
href={url}
isExternal
>
{url}
</Link>
</Box>
<Flex alignItems="center" justifyContent="center" marginTop="16px">
<Icon as={PencilIcon} verticalAlign="bottom" />
<Card marginLeft="8px">Cannot Verify: to be implemented</Card>
</Flex>
</Box>
);
};
207 changes: 207 additions & 0 deletions apps/web/src/components/WalletConnect/SessionProposalModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {
Box,
Button,
Card,
Divider,
FormControl,
FormErrorMessage,
HStack,
Icon,
ModalBody,
ModalContent,
ModalFooter,
Text,
VStack,
useToast,
} from "@chakra-ui/react";
import { type WalletKitTypes } from "@reown/walletkit";
import { useDynamicModalContext } from "@umami/components";
import {
useAsyncActionHandler,
useAvailableNetworks,
useGetImplicitAccount,
walletKit,
} from "@umami/state";
import { type Network } from "@umami/tezos";
import { buildApprovedNamespaces, getSdkError } from "@walletconnect/utils";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";

import { CheckmarkIcon, CloseIcon } from "../../assets/icons";
import { OwnedImplicitAccountsAutocomplete } from "../AddressAutocomplete";
import { ProjectInfoCard } from "./ProjectInfoCard";
import { VerifyInfobox } from "./VerifyInfobox";

export const SessionProposalModal = ({
proposal,
}: {
proposal: WalletKitTypes.SessionProposal;
}) => {
const getAccount = useGetImplicitAccount();
const availableNetworks: Network[] = useAvailableNetworks();
const toast = useToast();

const { onClose } = useDynamicModalContext();
const { handleAsyncAction } = useAsyncActionHandler();

const [isLoadingApprove, setIsLoadingApprove] = useState(false);

const form = useForm<{ address: string }>({
mode: "onBlur",
});
const {
getValues,
formState: { errors, isValid },
} = form;

// dApp sends in the session proposal the required networks and the optional networks.
// The response must contain all the required networks but Umami supports just one per request.
// So if the list of required networks is more than one or the required network is not supported, we can only reject the proposal.
const requiredNetworks = Object.entries(proposal.params.requiredNamespaces)
.map(([key, values]) => (key.includes(":") ? key : values.chains))
.flat();

let network = undefined;
let error = undefined;
if (requiredNetworks.length !== 1 || requiredNetworks[0] === undefined) {
error = "Expected only one required network, got " + requiredNetworks;
} else {
network = requiredNetworks[0];
const availablenetworks = availableNetworks.map(network => network.name);
if (!availablenetworks.includes(network.split(":")[1])) {
// the network contains a namespace, e.g. tezos:mainnet
error = `The required network ${network} is not supported. Available: ${availablenetworks}`;
}
}

if (error) {
console.error(error);
toast({ description: error, status: "error" });
}

const onApprove = () =>
handleAsyncAction(async () => {
setIsLoadingApprove(true);
const account = getAccount(getValues().address);

try {
const namespaces = buildApprovedNamespaces({
proposal: proposal.params,
supportedNamespaces: {
tezos: {
chains: [network ?? ""],
methods: ["tezos_getAccounts", "tezos_sign", "tezos_send"],
events: [],
accounts: [`${network}:${account.address.pkh}`],
},
},
});

await walletKit.approveSession({
id: proposal.id,
namespaces,
sessionProperties: {},
});
onClose();
} catch (e) {
toast({ description: (e as Error).message, status: "error" });
setIsLoadingApprove(false);
// keeping the modal open to show that the approval failed
return;
}
});

// Handle reject action
const onReject = () =>
handleAsyncAction(async () => {
// close immediately assuming that the user wants to get rid of the modal
onClose();
try {
await walletKit.rejectSession({
id: proposal.id,
reason: getSdkError("USER_REJECTED_METHODS"),
});
} catch (e) {
toast({ description: (e as Error).message, status: "error" });
return;
}
});

return (
<FormProvider {...form}>
<ModalContent>
<ModalBody>
<Card>
<ProjectInfoCard metadata={proposal.params.proposer.metadata} />
<Divider />
<Box marginBottom="16px" fontSize="xl" fontWeight="semibold">
Requested permissions
</Box>

<VStack align="start" spacing="8px">
<HStack>
<Icon as={CheckmarkIcon} />
<Card marginLeft="8px">View your balance and activity</Card>
</HStack>
<HStack>
<Icon as={CheckmarkIcon} />
<Card marginLeft="8px">Send approval requests</Card>
</HStack>
<HStack color="gray.500">
<Icon as={CloseIcon} />
<Card marginLeft="8px">Move funds without permission</Card>
</HStack>
</VStack>

<Box marginTop="8px">
{network ? (
<>
<FormControl marginTop="24px" isInvalid={!!errors.address}>
<OwnedImplicitAccountsAutocomplete
allowUnknown={false}
inputName="address"
label="Select Account"
/>
{errors.address && (
<FormErrorMessage>{errors.address.message}</FormErrorMessage>
)}
</FormControl>
<Text marginTop="16px" color="gray.500">
Network:
</Text>
<Text marginLeft="8px">{network}</Text>
</>
) : (
<>
<Text color="gray.600">Accounts</Text>
<Text>None available</Text>
<Text marginTop="16px" color="gray.500">
Network
</Text>
<Text>None of the required networks is supported</Text>
</>
)}
</Box>
<Divider />
<VerifyInfobox />
</Card>
</ModalBody>
<ModalFooter>
<Button width="100%" isDisabled={isLoadingApprove} onClick={onReject} size="lg">
Reject
</Button>
<Button
width="100%"
isDisabled={!!error || !isValid}
isLoading={isLoadingApprove}
loadingText="Approving..."
onClick={onApprove}
size="lg"
>
Approve
</Button>
</ModalFooter>
</ModalContent>
</FormProvider>
);
};
17 changes: 17 additions & 0 deletions apps/web/src/components/WalletConnect/VerifyInfobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Box, Card, HStack, Icon, VStack } from "@chakra-ui/react";

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

export const VerifyInfobox = () => (
<Box textAlign="center">
<VStack spacing="16px">
<HStack margin="auto">
<Icon as={AlertCircleIcon} verticalAlign="bottom" />
<Card marginLeft="8px">Unknown domain</Card>
</HStack>
<Box margin="auto">
<Card>This domain was not verified. To be implemented.</Card>
</Box>
</VStack>
</Box>
);
Loading

0 comments on commit ffcbede

Please sign in to comment.