Skip to content

Commit

Permalink
Merge pull request #1959 from trilitech/add-wc-connect
Browse files Browse the repository at this point in the history
feat: WalletConnect integration, part 1, session proposal
  • Loading branch information
dianasavvatina authored Dec 2, 2024
2 parents df8a19e + 9d8ec00 commit fba16b3
Show file tree
Hide file tree
Showing 19 changed files with 998 additions and 9 deletions.
12 changes: 12 additions & 0 deletions apps/desktop/src/setupTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,15 @@ jest.spyOn(console, "error").mockImplementation((...args) => {
}
originalError(...args);
});

jest.mock("@walletconnect/core", () => ({
Core: jest.fn().mockImplementation(config => ({
projectId: config.projectId,
})),
}));
jest.mock("@reown/walletkit", () => ({
WalletKit: jest.fn(),
}));
jest.mock("@walletconnect/utils", () => ({
WalletConnect: jest.fn(),
}));
4 changes: 4 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@emotion/styled": "^11.13.5",
"@hookform/resolvers": "^3.9.1",
"@reduxjs/toolkit": "^2.3.0",
"@reown/walletkit": "^1.0.1",
"@tanstack/react-query": "^5.61.5",
"@taquito/beacon-wallet": "^20.1.0",
"@taquito/ledger-signer": "^20.1.0",
Expand All @@ -50,6 +51,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
22 changes: 22 additions & 0 deletions apps/web/src/components/App/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,29 @@ jest.mock("@chakra-ui/react", () => ({
useBreakpointValue: jest.fn(map => map["lg"]),
}));

jest.mock("@umami/state", () => {
const mockedEmitter = {
removeAllListeners: jest.fn(),
};
return {
...jest.requireActual("@umami/state"),
walletKit: {
core: {},
metadata: {
name: "Umami Wallet",
description: "Umami Wallet with WalletConnect",
url: "https://umamiwallet.com",
icons: ["https://umamiwallet.com/assets/favicon-32-45gq0g6M.png"],
},
on: jest.fn().mockReturnValue(mockedEmitter),
},
createWalletKit: jest.fn(),
};
});

describe("<App />", () => {
afterEach(() => jest.restoreAllMocks());

it("renders welcome screen for a new user", () => {
render(<App />);

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
14 changes: 11 additions & 3 deletions apps/web/src/components/Menu/AppsMenu/AppsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,31 @@ import { Button, Text } from "@chakra-ui/react";
import { useAddPeer } from "@umami/state";

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

export const AppsMenu = () => {
const addPeer = useAddPeer();
const onBeaconConnect = useAddPeer();
const onWalletConnect = useOnWalletConnect();

return (
<DrawerContentWrapper
actions={
<>
<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:") ? onWalletConnect(payload) : onBeaconConnect(payload)
)
}
variant="primary"
>
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, 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">
<Card data-testid="session-info-card-text">
<Text as="span" fontWeight="bold">
{name}
</Text>
<Text size="md">wants to {intention ?? "connect"}</Text>
</Card>
</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>
);
};
149 changes: 149 additions & 0 deletions apps/web/src/components/WalletConnect/SessionProposalModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { type NetworkType } from "@airgap/beacon-wallet";
import {
Box,
Button,
Card,
Divider,
FormControl,
FormErrorMessage,
HStack,
Icon,
ModalBody,
ModalContent,
ModalFooter,
Text,
VStack,
} from "@chakra-ui/react";
import { type WalletKitTypes } from "@reown/walletkit";
import { useDynamicModalContext } from "@umami/components";
import { useAsyncActionHandler, useGetImplicitAccount, walletKit } from "@umami/state";
import { type SessionTypes } from "@walletconnect/types";
import { buildApprovedNamespaces, getSdkError } from "@walletconnect/utils";
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";
import { useColor } from "../../styles/useColor";

export const SessionProposalModal = ({
proposal,
network,
}: {
proposal: WalletKitTypes.SessionProposal;
network: NetworkType;
}) => {
const getAccount = useGetImplicitAccount();
const color = useColor();

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

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

const onApprove = () =>
handleAsyncAction(async () => {
const account = getAccount(getValues("address"));

// prepare the list of accounts and networks to approve
const namespaces = buildApprovedNamespaces({
proposal: proposal.params,
supportedNamespaces: {
tezos: {
chains: [network],
methods: ["tezos_getAccounts", "tezos_sign", "tezos_send"],
events: [],
accounts: [`${network}:${account.address.pkh}`],
},
},
});

const session: SessionTypes.Struct = await walletKit.approveSession({
id: proposal.id,
namespaces,
sessionProperties: {},
});
console.log("WC session approved", session);
onClose();
});

const onReject = () =>
handleAsyncAction(async () => {
// close immediately assuming that the user wants to get rid of the modal
onClose();
await walletKit.rejectSession({
id: proposal.id,
reason: getSdkError("USER_REJECTED_METHODS"),
});
});

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={color("500")}>
<Icon as={CloseIcon} />
<Card marginLeft="8px">Move funds without permission</Card>
</HStack>
</VStack>

<Box marginTop="8px">
<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={color("500")}>
Network:
</Text>
<Text marginLeft="8px">{network}</Text>
</Box>
<Divider />
<VerifyInfobox />
</Card>
</ModalBody>
<ModalFooter>
<Button width="100%" isDisabled={isLoading} onClick={onReject} size="lg">
Reject
</Button>
<Button
width="100%"
isDisabled={!isValid}
isLoading={isLoading}
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 fba16b3

Please sign in to comment.