From d84142d4885aff1105b126423d285237752987e5 Mon Sep 17 00:00:00 2001 From: Diana Savatina Date: Tue, 22 Oct 2024 14:45:44 +0100 Subject: [PATCH] feat: WalletConnect integration, request requests are supported. Tested: - send tez - delegate / UndelegationSignPage - originate / call contract - stake / unstake / finalize unstake --- apps/desktop/src/Router.tsx | 11 +- .../SendFlow/Beacon/useSignWithBeacon.tsx | 6 +- .../SendFlow/WalletConnect/useSignWithWc.tsx | 53 ++++++++ .../components/SendFlow/sdk/BatchSignPage.tsx | 6 +- .../SendFlow/sdk/ContractCallSignPage.tsx | 16 ++- .../SendFlow/sdk/DelegationSignPage.tsx | 16 ++- .../SendFlow/sdk/FinalizeUnstakeSignPage.tsx | 16 ++- .../src/components/SendFlow/sdk/Header.tsx | 2 +- .../sdk/OriginationOperationSignPage.tsx | 18 ++- .../SendFlow/sdk/SingleSignPage.tsx | 7 +- .../components/SendFlow/sdk/StakeSignPage.tsx | 16 ++- .../components/SendFlow/sdk/TezSignPage.tsx | 17 ++- .../SendFlow/sdk/UndelegationSignPage.tsx | 16 ++- .../SendFlow/sdk/UnstakeSignPage.tsx | 16 ++- apps/web/src/components/SendFlow/utils.tsx | 5 +- .../WalletConnect/ProjectInfoCard.tsx | 8 +- .../WalletConnect/SessionProposalModal.tsx | 12 +- .../WalletConnect/WalletConnectPeers.tsx | 78 +++++------ .../WalletConnect/WalletConnectProvider.tsx | 117 +++++++++-------- .../WalletConnect/useHandleWcRequest.tsx | 123 ++++++++++++++++++ .../beacon/useHandleBeaconMessage.tsx | 8 +- packages/state/src/hooks/WalletConnect.ts | 86 ++---------- packages/state/src/hooks/network.ts | 6 +- packages/state/src/slices/WalletConnect.ts | 30 +---- 24 files changed, 412 insertions(+), 277 deletions(-) create mode 100644 apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx create mode 100644 apps/web/src/components/WalletConnect/useHandleWcRequest.tsx diff --git a/apps/desktop/src/Router.tsx b/apps/desktop/src/Router.tsx index c82bd67eb9..be27e89db9 100644 --- a/apps/desktop/src/Router.tsx +++ b/apps/desktop/src/Router.tsx @@ -1,12 +1,7 @@ /* istanbul ignore file */ import { DynamicModalContext, useDynamicModal } from "@umami/components"; import { useDataPolling } from "@umami/data-polling"; -import { - WalletClient, - useImplicitAccounts, - useResetBeaconConnections, - useResetWcConnections, -} 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"; @@ -65,12 +60,10 @@ const LoggedInRouterWithPolling = () => { const LoggedOutRouter = () => { const resetBeaconConnections = useResetBeaconConnections(); - const resetWcConnections = useResetWcConnections(); useEffect(() => { WalletClient.destroy().then(resetBeaconConnections).catch(noop); - resetWcConnections(); - }, [resetBeaconConnections, resetWcConnections]); + }, [resetBeaconConnections]); return ( diff --git a/apps/web/src/components/SendFlow/Beacon/useSignWithBeacon.tsx b/apps/web/src/components/SendFlow/Beacon/useSignWithBeacon.tsx index 579724d9ff..7def3c8f69 100644 --- a/apps/web/src/components/SendFlow/Beacon/useSignWithBeacon.tsx +++ b/apps/web/src/components/SendFlow/Beacon/useSignWithBeacon.tsx @@ -2,7 +2,7 @@ import { BeaconMessageType, type OperationResponseInput } from "@airgap/beacon-w import { type TezosToolkit } from "@taquito/taquito"; import { useDynamicModalContext } from "@umami/components"; import { executeOperations, totalFee } from "@umami/core"; -import { WalletClient, useAsyncActionHandler, useFindNetwork } from "@umami/state"; +import { WalletClient, useAsyncActionHandler } from "@umami/state"; import { useForm } from "react-hook-form"; import { SuccessStep } from "../SuccessStep"; @@ -15,7 +15,6 @@ export const useSignWithBeacon = ({ }: SdkSignPageProps): CalculatedSignProps => { const { isLoading: isSigning, handleAsyncAction } = useAsyncActionHandler(); const { openWith } = useDynamicModalContext(); - const findNetwork = useFindNetwork(); const form = useForm({ defaultValues: { executeParams: operation.estimates } }); @@ -45,7 +44,6 @@ export const useSignWithBeacon = ({ fee: totalFee(form.watch("executeParams")), isSigning, onSign, - network: findNetwork(headerProps.networkName), - form, + network: headerProps.network, }; }; diff --git a/apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx b/apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx new file mode 100644 index 0000000000..e3f688526c --- /dev/null +++ b/apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx @@ -0,0 +1,53 @@ +import { type TezosToolkit } from "@taquito/taquito"; +import { useDynamicModalContext } from "@umami/components"; +import { executeOperations, totalFee } from "@umami/core"; +import { useAsyncActionHandler, walletKit } from "@umami/state"; +import { formatJsonRpcResult } from "@walletconnect/jsonrpc-utils"; +import { useForm } from "react-hook-form"; + +import { SuccessStep } from "../SuccessStep"; +import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; + +export const useSignWithWalletConnect = ({ + operation, + headerProps, + requestId, +}: SdkSignPageProps): CalculatedSignProps => { + const { isLoading: isSigning, handleAsyncAction } = useAsyncActionHandler(); + const { openWith } = useDynamicModalContext(); + + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); + + if (requestId.sdkType !== "walletconnect") { + return { + fee: 0, + isSigning: false, + onSign: async () => {}, + network: null, + }; + } + + const onSign = async (tezosToolkit: TezosToolkit) => + handleAsyncAction( + async () => { + const { opHash } = await executeOperations( + { ...operation, estimates: form.watch("executeParams") }, + tezosToolkit + ); + + const response = formatJsonRpcResult(requestId.id, { hash: opHash }); + await walletKit.respondSessionRequest({ topic: requestId.topic, response }); + return openWith(); + }, + error => ({ + description: `Failed to confirm Beacon operation: ${error.message}`, + }) + ); + + return { + fee: totalFee(form.watch("executeParams")), + isSigning, + onSign, + network: headerProps.network, + }; +}; diff --git a/apps/web/src/components/SendFlow/sdk/BatchSignPage.tsx b/apps/web/src/components/SendFlow/sdk/BatchSignPage.tsx index 8616529ef8..5e8858db05 100644 --- a/apps/web/src/components/SendFlow/sdk/BatchSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/BatchSignPage.tsx @@ -23,6 +23,7 @@ import { useSignWithBeacon } from "../Beacon/useSignWithBeacon"; import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type SdkSignPageProps } from "../utils"; +import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc"; export const BatchSignPage = ( signProps: SdkSignPageProps, @@ -30,7 +31,10 @@ export const BatchSignPage = ( ) => { const color = useColor(); - const calculatedProps = useSignWithBeacon({ ...signProps }); + const beaconCalculatedProps = useSignWithBeacon({ ...signProps }); + const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps }); + const calculatedProps = + signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps; const { isSigning, onSign, network, fee, form } = calculatedProps; const { signer, operations } = signProps.operation; diff --git a/apps/web/src/components/SendFlow/sdk/ContractCallSignPage.tsx b/apps/web/src/components/SendFlow/sdk/ContractCallSignPage.tsx index b9b261fbd7..9af9172441 100644 --- a/apps/web/src/components/SendFlow/sdk/ContractCallSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/ContractCallSignPage.tsx @@ -12,7 +12,7 @@ import { ModalFooter, } from "@chakra-ui/react"; import { type ContractCall } from "@umami/core"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { Header } from "./Header"; import { useColor } from "../../../styles/useColor"; @@ -24,10 +24,14 @@ import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; -export const ContractCallSignPage = ( - { operation, headerProps }: SdkSignPageProps, - calculatedSignProps: CalculatedSignProps -) => { +export const ContractCallSignPage = ({ + operation, + headerProps, + isSigning, + onSign, + network, + fee, +}: SdkSignPageProps & CalculatedSignProps) => { const { amount: mutezAmount, contract, @@ -35,7 +39,7 @@ export const ContractCallSignPage = ( args, } = operation.operations[0] as ContractCall; const color = useColor(); - const { isSigning, onSign, network, fee, form } = calculatedSignProps; + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); return ( diff --git a/apps/web/src/components/SendFlow/sdk/DelegationSignPage.tsx b/apps/web/src/components/SendFlow/sdk/DelegationSignPage.tsx index b41e476ed9..8ab37e4382 100644 --- a/apps/web/src/components/SendFlow/sdk/DelegationSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/DelegationSignPage.tsx @@ -1,6 +1,6 @@ import { Flex, FormLabel, ModalBody, ModalContent, ModalFooter } from "@chakra-ui/react"; import { type Delegation } from "@umami/core"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { Header } from "./Header"; import { AddressTile } from "../../AddressTile/AddressTile"; @@ -9,13 +9,17 @@ import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; -export const DelegationSignPage = ( - { operation, headerProps }: SdkSignPageProps, - calculatedSignProps: CalculatedSignProps -) => { +export const DelegationSignPage = ({ + operation, + headerProps, + isSigning, + onSign, + network, + fee, +}: SdkSignPageProps & CalculatedSignProps) => { const { recipient } = operation.operations[0] as Delegation; - const { isSigning, onSign, network, fee, form } = calculatedSignProps; + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); return ( diff --git a/apps/web/src/components/SendFlow/sdk/FinalizeUnstakeSignPage.tsx b/apps/web/src/components/SendFlow/sdk/FinalizeUnstakeSignPage.tsx index 76b2969117..cd1e081e86 100644 --- a/apps/web/src/components/SendFlow/sdk/FinalizeUnstakeSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/FinalizeUnstakeSignPage.tsx @@ -1,6 +1,6 @@ import { Flex, FormLabel, ModalBody, ModalContent, ModalFooter } from "@chakra-ui/react"; import { useAccountTotalFinalizableUnstakeAmount } from "@umami/state"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { Header } from "./Header"; import { AddressTile } from "../../AddressTile/AddressTile"; @@ -9,11 +9,15 @@ import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; -export const FinalizeUnstakeSignPage = ( - { operation, headerProps }: SdkSignPageProps, - calculatedSignProps: CalculatedSignProps -) => { - const { isSigning, onSign, network, fee, form } = calculatedSignProps; +export const FinalizeUnstakeSignPage = ({ + operation, + headerProps, + isSigning, + onSign, + network, + fee, +}: SdkSignPageProps & CalculatedSignProps) => { + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); const totalFinalizableAmount = useAccountTotalFinalizableUnstakeAmount( operation.signer.address.pkh ); diff --git a/apps/web/src/components/SendFlow/sdk/Header.tsx b/apps/web/src/components/SendFlow/sdk/Header.tsx index 7256daa035..c032a227d6 100644 --- a/apps/web/src/components/SendFlow/sdk/Header.tsx +++ b/apps/web/src/components/SendFlow/sdk/Header.tsx @@ -16,7 +16,7 @@ export const Header = ({ headerProps }: { headerProps: SignHeaderProps }) => { Network: - {capitalize(headerProps.networkName)} + {capitalize(headerProps.network.name)} diff --git a/apps/web/src/components/SendFlow/sdk/OriginationOperationSignPage.tsx b/apps/web/src/components/SendFlow/sdk/OriginationOperationSignPage.tsx index e9d5431bda..ee65587e93 100644 --- a/apps/web/src/components/SendFlow/sdk/OriginationOperationSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/OriginationOperationSignPage.tsx @@ -17,7 +17,7 @@ import { } from "@chakra-ui/react"; import { type ContractOrigination } from "@umami/core"; import { capitalize } from "lodash"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { CodeSandboxIcon } from "../../../assets/icons"; import { useColor } from "../../../styles/useColor"; @@ -27,13 +27,17 @@ import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; -export const OriginationOperationSignPage = ( - { operation, headerProps }: SdkSignPageProps, - calculatedSignProps: CalculatedSignProps -) => { - const { isSigning, onSign, network, form, fee } = calculatedSignProps; +export const OriginationOperationSignPage = ({ + operation, + headerProps, + isSigning, + onSign, + network, + fee, +}: SdkSignPageProps & CalculatedSignProps) => { const color = useColor(); const { code, storage } = operation.operations[0] as ContractOrigination; + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); return ( @@ -48,7 +52,7 @@ export const OriginationOperationSignPage = ( Network: - {capitalize(headerProps.networkName)} + {capitalize(headerProps.network.name)} diff --git a/apps/web/src/components/SendFlow/sdk/SingleSignPage.tsx b/apps/web/src/components/SendFlow/sdk/SingleSignPage.tsx index c51abd0d7a..b1160855aa 100644 --- a/apps/web/src/components/SendFlow/sdk/SingleSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/SingleSignPage.tsx @@ -8,11 +8,16 @@ import { TezSignPage } from "./TezSignPage"; import { UndelegationSignPage } from "./UndelegationSignPage"; import { UnstakeSignPage } from "./UnstakeSignPage"; import { useSignWithBeacon } from "../Beacon/useSignWithBeacon"; +import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc"; export const SingleSignPage = (signProps: SdkSignPageProps) => { const operationType = signProps.operation.operations[0].type; - const calculatedProps = useSignWithBeacon({ ...signProps }); + const beaconCalculatedProps = useSignWithBeacon({ ...signProps }); + const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps }); + const calculatedProps = + signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps; + console.log("SingleSignPage, signProps, calculatedProps", signProps, calculatedProps); switch (operationType) { case "tez": { diff --git a/apps/web/src/components/SendFlow/sdk/StakeSignPage.tsx b/apps/web/src/components/SendFlow/sdk/StakeSignPage.tsx index 025cf195be..237dfe4b84 100644 --- a/apps/web/src/components/SendFlow/sdk/StakeSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/StakeSignPage.tsx @@ -1,6 +1,6 @@ import { Flex, FormLabel, ModalBody, ModalContent, ModalFooter } from "@chakra-ui/react"; import { type Stake } from "@umami/core"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { Header } from "./Header"; import { AddressTile } from "../../AddressTile/AddressTile"; @@ -9,13 +9,17 @@ import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; -export const StakeSignPage = ( - { operation, headerProps }: SdkSignPageProps, - calculatedSignProps: CalculatedSignProps -) => { +export const StakeSignPage = ({ + operation, + headerProps, + isSigning, + onSign, + network, + fee, +}: SdkSignPageProps & CalculatedSignProps) => { const { amount: mutezAmount } = operation.operations[0] as Stake; - const { isSigning, onSign, network, fee, form } = calculatedSignProps; + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); return ( diff --git a/apps/web/src/components/SendFlow/sdk/TezSignPage.tsx b/apps/web/src/components/SendFlow/sdk/TezSignPage.tsx index f60792f4df..0d033b7730 100644 --- a/apps/web/src/components/SendFlow/sdk/TezSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/TezSignPage.tsx @@ -1,6 +1,6 @@ import { Flex, FormLabel, ModalBody, ModalContent, ModalFooter } from "@chakra-ui/react"; import { type TezTransfer } from "@umami/core"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { Header } from "./Header"; import { AddressTile } from "../../AddressTile/AddressTile"; @@ -10,13 +10,16 @@ import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; -export const TezSignPage = ( - { operation, headerProps }: SdkSignPageProps, - calculatedSignProps: CalculatedSignProps -) => { +export const TezSignPage = ({ + operation, + headerProps, + isSigning, + onSign, + network, + fee, +}: SdkSignPageProps & CalculatedSignProps) => { const { amount: mutezAmount, recipient } = operation.operations[0] as TezTransfer; - - const { isSigning, onSign, network, fee, form } = calculatedSignProps; + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); return ( diff --git a/apps/web/src/components/SendFlow/sdk/UndelegationSignPage.tsx b/apps/web/src/components/SendFlow/sdk/UndelegationSignPage.tsx index 977bea20c0..c7094ff075 100644 --- a/apps/web/src/components/SendFlow/sdk/UndelegationSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/UndelegationSignPage.tsx @@ -1,5 +1,5 @@ import { Flex, FormLabel, ModalBody, ModalContent, ModalFooter } from "@chakra-ui/react"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { Header } from "./Header"; import { AddressTile } from "../../AddressTile/AddressTile"; @@ -8,11 +8,15 @@ import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; -export const UndelegationSignPage = ( - { operation, headerProps }: SdkSignPageProps, - calculatedSignProps: CalculatedSignProps -) => { - const { isSigning, onSign, network, form, fee } = calculatedSignProps; +export const UndelegationSignPage = ({ + operation, + headerProps, + isSigning, + onSign, + network, + fee, +}: SdkSignPageProps & CalculatedSignProps) => { + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); return ( diff --git a/apps/web/src/components/SendFlow/sdk/UnstakeSignPage.tsx b/apps/web/src/components/SendFlow/sdk/UnstakeSignPage.tsx index 41c3088656..3bbb878cfd 100644 --- a/apps/web/src/components/SendFlow/sdk/UnstakeSignPage.tsx +++ b/apps/web/src/components/SendFlow/sdk/UnstakeSignPage.tsx @@ -1,6 +1,6 @@ import { Flex, FormLabel, ModalBody, ModalContent, ModalFooter } from "@chakra-ui/react"; import { type Unstake } from "@umami/core"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { Header } from "./Header"; import { AddressTile } from "../../AddressTile/AddressTile"; @@ -9,13 +9,17 @@ import { SignButton } from "../SignButton"; import { SignPageFee } from "../SignPageFee"; import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; -export const UnstakeSignPage = ( - { operation, headerProps }: SdkSignPageProps, - calculatedSignProps: CalculatedSignProps -) => { +export const UnstakeSignPage = ({ + operation, + headerProps, + isSigning, + onSign, + network, + fee, +}: SdkSignPageProps & CalculatedSignProps) => { const { amount: mutezAmount } = operation.operations[0] as Unstake; - const { isSigning, onSign, network, fee, form } = calculatedSignProps; + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); return ( diff --git a/apps/web/src/components/SendFlow/utils.tsx b/apps/web/src/components/SendFlow/utils.tsx index 5e3430361c..a2c639c54d 100644 --- a/apps/web/src/components/SendFlow/utils.tsx +++ b/apps/web/src/components/SendFlow/utils.tsx @@ -18,7 +18,7 @@ import { useGetOwnedAccount, useSelectedNetwork, } from "@umami/state"; -import { type ExecuteParams, type RawPkh } from "@umami/tezos"; +import { type ExecuteParams, type Network, type RawPkh } from "@umami/tezos"; import { repeat } from "lodash"; import { useState } from "react"; import { useForm, useFormContext } from "react-hook-form"; @@ -55,7 +55,6 @@ export type CalculatedSignProps = { isSigning: boolean; onSign: (tezosToolkit: TezosToolkit) => Promise; network: any; - form: any; }; export type sdkType = "beacon" | "walletconnect"; @@ -72,7 +71,7 @@ export type SignRequestId = }; export type SignHeaderProps = { - networkName: string; + network: Network; appName: string; appIcon?: string; }; diff --git a/apps/web/src/components/WalletConnect/ProjectInfoCard.tsx b/apps/web/src/components/WalletConnect/ProjectInfoCard.tsx index 7f7a8ea6c4..09c5bd7e82 100644 --- a/apps/web/src/components/WalletConnect/ProjectInfoCard.tsx +++ b/apps/web/src/components/WalletConnect/ProjectInfoCard.tsx @@ -1,4 +1,4 @@ -import { Avatar, Box, Card, Flex, Heading, Icon, Link, Text } from "@chakra-ui/react"; +import { Avatar, Box, Card, Flex, Icon, Link, Text } from "@chakra-ui/react"; import { type SignClientTypes } from "@walletconnect/types"; import { PencilIcon } from "../../assets/icons"; @@ -20,12 +20,12 @@ export const ProjectInfoCard = ({ metadata, intention }: Props) => { - + {name} {" "} - wants to {intention ?? "connect"} - + wants to {intention ?? "connect"} + { - const addConnectionToWcSlice = useAddWcConnection(); + // const addWcTopicToAcc = useAddWcTopicToAcc(); const getAccount = useGetImplicitAccount(); - const { refresh } = useWcPeers(); + const toggleWcPeerListUpdated = useToggleWcPeerListUpdated(); const { onClose } = useDynamicModalContext(); const { isLoading, handleAsyncAction } = useAsyncActionHandler(); @@ -77,8 +75,8 @@ export const SessionProposalModal = ({ namespaces, sessionProperties: {}, }); - await addConnectionToWcSlice(session, account.address.pkh, network.split(":")[1] as NetworkName); - await refresh(); + console.log("Approved session", session); + toggleWcPeerListUpdated(); onClose(); }); diff --git a/apps/web/src/components/WalletConnect/WalletConnectPeers.tsx b/apps/web/src/components/WalletConnect/WalletConnectPeers.tsx index 1e8e6e18f4..149705ee74 100644 --- a/apps/web/src/components/WalletConnect/WalletConnectPeers.tsx +++ b/apps/web/src/components/WalletConnect/WalletConnectPeers.tsx @@ -1,5 +1,5 @@ -import { Center, Divider, Flex, Heading, IconButton, Image, Text, VStack } from "@chakra-ui/react"; -import { useGetWcConnectionInfo, useRemoveWcPeer, useWcPeers } from "@umami/state"; +import { Center, Divider, Flex, IconButton, Image, Text, VStack } from "@chakra-ui/react"; +import { useGetWcPeerListToggle, useRemoveWcPeer, walletKit } from "@umami/state"; import { parsePkh } from "@umami/tezos"; import { type SessionTypes } from "@walletconnect/types"; import { getSdkError } from "@walletconnect/utils"; @@ -7,21 +7,19 @@ import capitalize from "lodash/capitalize"; import { CodeSandboxIcon, StubIcon as TrashIcon } from "../../assets/icons"; import { useColor } from "../../styles/useColor"; -import { AddressPill } from "../AddressPill/AddressPill"; +import { AddressPill } from "../AddressPill"; import { EmptyMessage } from "../EmptyMessage"; /** * Component displaying a list of connected dApps. * - * Loads dApps data from {@link useWcPeers} hook & zips it with generated dAppIds. + * Loads dApps data from WalletConnct API and displays it in a list. */ export const WcPeers = () => { - // const wcPeers: Record = walletKit.getActiveSessions(); - const { peers: wcPeers } = useWcPeers(); + const sessions: Record = walletKit.getActiveSessions(); + useGetWcPeerListToggle(); - console.log("wcPeers", wcPeers); - - if (Object.keys(wcPeers).length === 0) { + if (Object.keys(sessions).length === 0) { return ( { > { // loop peers and print PeerRow - Object.entries(wcPeers).map(([topic, peerInfo]) => ( - + Object.entries(sessions).map(([topic, sessionInfo]) => ( + )) } @@ -55,10 +53,10 @@ export const WcPeers = () => { /** * Component for displaying info about single connected dApp. * - * @param peerInfo - peerInfo provided by wc Api + computed dAppId. + * @param sessionInfo - sessionInfo provided by wc Api + computed dAppId. * @param onRemove - action for deleting dApp connection. */ -const PeerRow = ({ peerInfo }: { peerInfo: SessionTypes.Struct }) => { +const PeerRow = ({ sessionInfo }: { sessionInfo: SessionTypes.Struct }) => { const color = useColor(); const removeWcPeer = useRemoveWcPeer(); @@ -75,14 +73,14 @@ const PeerRow = ({ peerInfo }: { peerInfo: SessionTypes.Struct }) => { } - src={peerInfo.peer.metadata.icons[0]} + src={sessionInfo.peer.metadata.icons[0]} />
- - {peerInfo.peer.metadata.name} - - + + {sessionInfo.peer.metadata.name} + +
{ aria-label="Remove Peer" icon={} onClick={() => - removeWcPeer({ topic: peerInfo.topic, reason: getSdkError("USER_DISCONNECTED") }) + removeWcPeer({ topic: sessionInfo.topic, reason: getSdkError("USER_DISCONNECTED") }) } variant="iconButtonSolid" /> @@ -101,28 +99,24 @@ const PeerRow = ({ peerInfo }: { peerInfo: SessionTypes.Struct }) => { /** * 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}. + * Displays {@link AddressPill} with a connected account and network type. * - * @param peerInfo - peerInfo provided by wc Api + computed dAppId. + * @param sessionInfo - sessionInfo provided by WalletConnect Api. + * Account is stored in format: tezos:ghostnet:tz1... + * Network is stored in format: tezos:mainnet */ -const StoredPeerInfo = ({ peerInfo }: { peerInfo: SessionTypes.Struct }) => { - const connectionInfo = useGetWcConnectionInfo(peerInfo.topic); - - if (!connectionInfo) { - return null; - } - - return ( - - - - - Network: - - - {capitalize(connectionInfo.networkName)} - - - ); -}; +const StoredSessionInfo = ({ sessionInfo }: { sessionInfo: SessionTypes.Struct }) => ( + + + + + Network: + + + {capitalize(sessionInfo.namespaces.tezos.chains?.[0].split(":")[1] ?? "")} + + +); diff --git a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx index 54742e741b..15ae92280d 100644 --- a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx +++ b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx @@ -6,42 +6,29 @@ import { createWalletKit, useAsyncActionHandler, useAvailableNetworks, - useRemoveWcConnection, - useWcPeers, + useToggleWcPeerListUpdated, walletKit, } from "@umami/state"; import { type Network } from "@umami/tezos"; import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils"; +import { type SessionTypes } from "@walletconnect/types"; import { getSdkError } from "@walletconnect/utils"; import { type PropsWithChildren, useEffect } from "react"; import { SessionProposalModal } from "./SessionProposalModal"; +import { useHandleWcRequest } from "./useHandleWcRequest"; export const WalletConnectProvider = ({ children }: PropsWithChildren) => { - const onSessionProposal = useOnSessionProposal(); - const onSessionDelete = useOnSessionDelete(); - const onSessionRequest = useOnSessionRequest(); - - useEffect(() => { - const initializeWallet = async () => { - await createWalletKit(); - walletKit.on("session_proposal", event => void onSessionProposal(event)); - walletKit.on("session_request", event => void onSessionRequest(event)); - walletKit.on("session_delete", event => void onSessionDelete(event)); - }; - - void initializeWallet(); - }); - - return children; -}; - -const useOnSessionProposal = () => { const { handleAsyncActionUnsafe } = useAsyncActionHandler(); const { openWith } = useDynamicModalContext(); + const toggleWcPeerListUpdated = useToggleWcPeerListUpdated(); + const toast = useToast(); + const availableNetworks: Network[] = useAvailableNetworks(); - return (proposal: WalletKitTypes.SessionProposal) => + const handleWcRequest = useHandleWcRequest(); + + const onSessionProposal = (proposal: WalletKitTypes.SessionProposal) => handleAsyncActionUnsafe(async () => { // 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. @@ -66,50 +53,74 @@ const useOnSessionProposal = () => { } await openWith(, {}); + console.log("Session proposal from dApp", proposal, walletKit.getActiveSessions()); }).catch(async () => { // dApp is waiting so we need to notify it await walletKit.rejectSession({ id: proposal.id, reason: getSdkError("UNSUPPORTED_CHAINS") }); }); -}; -const useOnSessionRequest = () => { - const { handleAsyncAction } = useAsyncActionHandler(); - - return (event: WalletKitTypes.SessionRequest) => - handleAsyncAction(async () => { - console.log("TODO: Session request received. Handling to be implemented", event); - - const response = formatJsonRpcError(event.id, getSdkError("USER_REJECTED_METHODS").message); - await walletKit.respondSessionRequest({ topic: event.topic, response }); - }).catch(async () => { - // dApp is waiting so we need to notify it - const response = formatJsonRpcError(event.id, getSdkError("INVALID_METHOD").message); - await walletKit.respondSessionRequest({ topic: event.topic, response }); + const onSessionDelete = (event: WalletKitTypes.SessionDelete) => { + // by that time the session is already deleted from WalletKit so we cannot find the dApp name + console.log("Session deleted by dApp", event); + toast({ + description: "Session deleted by peer dApp", + status: "info", }); -}; + // no re-render peer list + toggleWcPeerListUpdated(); + }; -const useOnSessionDelete = () => { - const { handleAsyncAction } = useAsyncActionHandler(); - const { peers, refresh } = useWcPeers(); - const removeWcPeer = useRemoveWcConnection(); - const toast = useToast(); + const onSessionRequest = (event: WalletKitTypes.SessionRequest) => + handleAsyncActionUnsafe(async () => { + const activeSessions: Record = walletKit.getActiveSessions(); + if (!(event.topic in activeSessions)) { + console.error("WalletConnect session request failed. Session not found", event); + throw new Error("WalletConnect session request failed. Session not found"); + } - return (event: WalletKitTypes.SessionDelete) => - handleAsyncAction(async () => { - const { topic } = event; - if (topic in peers) { + const session = activeSessions[event.topic]; + + console.log("Session request, event, session", event, session); + console.log("Session request from dApp", session); + toast({ + description: `Session request from dApp ${session.peer.metadata.name}`, + status: "info", + }); + await handleWcRequest(event, session); + }).catch(async error => { + const { id, topic } = event; + const activeSessions: Record = walletKit.getActiveSessions(); + console.error("WalletConnect session request failed", event, error); + if (event.topic in activeSessions) { + const session = activeSessions[event.topic]; toast({ - description: `Session deleted by dApp ${peers[topic].peer.metadata.name}`, - status: "info", + description: `Session request for dApp ${session.peer.metadata.name} failed. It was rejected.`, + status: "error", }); } else { - console.error(`Session deleted by dApp but not known locally. Topic: ${topic}`); + toast({ + description: `Session request for dApp ${topic} failed. It was rejected. Peer not found by topic.`, + status: "error", + }); } - removeWcPeer(topic); - - // update peer list in the UI - await refresh(); + // dApp is waiting so we need to notify it + const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message); + await walletKit.respondSessionRequest({ topic, response }); }); + + useEffect(() => { + const initializeWallet = async () => { + // create the waalet just once + await createWalletKit(); + walletKit.on("session_proposal", event => void onSessionProposal(event)); + walletKit.on("session_request", event => void onSessionRequest(event)); + walletKit.on("session_delete", event => void onSessionDelete(event)); + }; + void initializeWallet(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return children; }; export const useOnWalletConnect = () => { diff --git a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx new file mode 100644 index 0000000000..cb888ebc71 --- /dev/null +++ b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx @@ -0,0 +1,123 @@ +import { useToast } from "@chakra-ui/react"; +import { useDynamicModalContext } from "@umami/components"; +import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core"; +import { + useAsyncActionHandler, + useFindNetwork, + useGetOwnedAccountSafe, + walletKit, +} from "@umami/state"; +import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils"; +import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types"; +import { getSdkError } from "@walletconnect/utils"; + +import { BatchSignPage } from "../SendFlow/sdk/BatchSignPage"; +import { SingleSignPage } from "../SendFlow/sdk/SingleSignPage"; +import { type SdkSignPageProps, type SignHeaderProps } from "../SendFlow/utils"; + +/** + * @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, + * estimate the fee and open the BeaconSignPage only if it succeeds + */ +export const useHandleWcRequest = () => { + const { openWith } = useDynamicModalContext(); + const { handleAsyncActionUnsafe } = useAsyncActionHandler(); + const getAccount = useGetOwnedAccountSafe(); + const findNetwork = useFindNetwork(); + const toast = useToast(); + + return async ( + event: { + verifyContext: Verify.Context; + } & SignClientTypes.BaseEventArgs<{ + request: { + method: string; + params: any; + expiryTimestamp?: number; + }; + chainId: string; + }>, + session: SessionTypes.Struct + ) => { + await handleAsyncActionUnsafe( + async () => { + const { id, topic, params } = event; + const { request, chainId } = params; + + let modal; + let onClose; + + switch (request.method) { + case "tezos_getAccounts": { + const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message); + await walletKit.respondSessionRequest({ topic, response }); + return; + } + + case "tezos_sign": { + // onClose = async () => { + // const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message); + // await walletKit.respondSessionRequest({ topic, response }); + // }; + // return openWith(, { onClose }); + const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message); + await walletKit.respondSessionRequest({ topic, response }); + return; + } + + case "tezos_send": { + if (!request.params.account) { + throw new Error("Missing account in request"); + } + const signer = getAccount(request.params.account); + if (!signer) { + throw new Error(`Unknown account, no signer: ${request.params.account}`); + } + const operation = toAccountOperations( + request.params.operations, + signer as ImplicitAccount + ); + const network = findNetwork(chainId.split(":")[1]); + if (!network) { + const response = formatJsonRpcError(id, getSdkError("INVALID_EVENT").message); + await walletKit.respondSessionRequest({ topic, response }); + toast({ description: `Unsupported network: ${chainId}`, status: "error" }); + return; + } + const estimatedOperations = await estimate(operation, network); + console.log("got request", request); + const headerProps: SignHeaderProps = { + network, + appName: session.peer.metadata.name, + appIcon: session.peer.metadata.icons[0], + }; + const signProps: SdkSignPageProps = { + headerProps: headerProps, + operation: estimatedOperations, + requestId: { sdkType: "walletconnect", id: id, topic }, + }; + + if (operation.operations.length === 1) { + modal = ; + } else { + modal = ; + } + onClose = async () => { + const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message); + await walletKit.respondSessionRequest({ topic, response }); + }; + + return openWith(modal, { onClose }); + } + default: + throw new Error(`Unsupported method ${request.method}`); + } + } + // error => ({ + // description: `Error while processing WalletConnect request: ${error.message}`, + // }) + ); + }; +}; diff --git a/apps/web/src/components/beacon/useHandleBeaconMessage.tsx b/apps/web/src/components/beacon/useHandleBeaconMessage.tsx index 49dc19fac8..2e838abc35 100644 --- a/apps/web/src/components/beacon/useHandleBeaconMessage.tsx +++ b/apps/web/src/components/beacon/useHandleBeaconMessage.tsx @@ -19,7 +19,7 @@ import { PermissionRequestModal } from "./PermissionRequestModal"; import { SignPayloadRequestModal } from "./SignPayloadRequestModal"; import { BatchSignPage } from "../../components/SendFlow/sdk/BatchSignPage"; import { SingleSignPage } from "../../components/SendFlow/sdk/SingleSignPage"; -import { type SdkSignPageProps } from "../SendFlow/utils"; +import { type SdkSignPageProps, type SignHeaderProps } from "../SendFlow/utils"; /** * @returns a function that handles a beacon message and opens a modal with the appropriate content @@ -103,14 +103,14 @@ export const useHandleBeaconMessage = () => { }); throw new Error(`Unknown account: ${message.sourceAddress}`); } + const operation = toAccountOperations( message.operationDetails, signer as ImplicitAccount ); const estimatedOperations = await estimate(operation, network); - const headerProps = { - requestId: message.id, - networkName: message.network.type, + const headerProps: SignHeaderProps = { + network: network, appName: message.appMetadata.name, appIcon: message.appMetadata.icon, }; diff --git a/packages/state/src/hooks/WalletConnect.ts b/packages/state/src/hooks/WalletConnect.ts index 615725e286..f5878134f6 100644 --- a/packages/state/src/hooks/WalletConnect.ts +++ b/packages/state/src/hooks/WalletConnect.ts @@ -1,87 +1,27 @@ -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 { 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]; +// get a toggle to monitor updates in peer list +export const useGetWcPeerListToggle = () => { + const wcData = useAppSelector(s => s.walletconnect); + return wcData.peerListUpdatedToggle; }; -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) => { - console.log("adding WC connection", session.topic, accountPkh, chain); - dispatch(wcActions.addConnection({ topic: session.topic, accountPkh, networkName: chain })); - void refresh(); - }; -}; - -/** - * Returns function for removing connection from {@link wcSlice}. - */ -export const useRemoveWcConnection = () => { +// report that the peer list is updated +export const useToggleWcPeerListUpdated = () => { 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 }; + return () => dispatch(wcActions.togglePeerListUpdated()); }; +// remove dApp connection from WalletConnect SDK 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()); + const togglePeerListUpdated = useToggleWcPeerListUpdated(); + return async (params: { topic: string; reason: ErrorResponse }) => { + console.log("disconnecting WC session on user request", params); + await walletKit.disconnectSession(params).then(async () => togglePeerListUpdated()); }; }; diff --git a/packages/state/src/hooks/network.ts b/packages/state/src/hooks/network.ts index 232542ea97..3e6ba11cdc 100644 --- a/packages/state/src/hooks/network.ts +++ b/packages/state/src/hooks/network.ts @@ -10,8 +10,10 @@ export const useAvailableNetworks = () => useAppSelector(s => s.networks.availab export const useFindNetwork = () => { const availableNetworks = useAvailableNetworks(); - return (name: string) => - availableNetworks.find(network => network.name.toLowerCase() === name.toLowerCase()); + return (name: string) => { + console.log("availableNetworks", availableNetworks); + return availableNetworks.find(network => network.name.toLowerCase() === name.toLowerCase()); + }; }; /** diff --git a/packages/state/src/slices/WalletConnect.ts b/packages/state/src/slices/WalletConnect.ts index ebb08dde2e..ff40d581e9 100644 --- a/packages/state/src/slices/WalletConnect.ts +++ b/packages/state/src/slices/WalletConnect.ts @@ -1,16 +1,13 @@ import { createSlice } from "@reduxjs/toolkit"; -import { type NetworkName, type RawPkh } from "@umami/tezos"; -import { fromPairs } from "lodash"; -export type DAppWcConnectionInfo = { - accountPkh: RawPkh; - networkName: NetworkName; +export type WalletConnectInfo = { + peerListUpdatedToggle: boolean; }; // mapping topic -> connection info -type State = Record; +export type State = WalletConnectInfo; -export const wcInitialState: State = {}; +export const wcInitialState: State = { peerListUpdatedToggle: false }; /** * Stores connection info between dApps and accounts. @@ -23,23 +20,10 @@ export const wcSlice = createSlice({ reducers: { reset: () => wcInitialState, - addConnection: ( - state, - { payload }: { payload: { topic: string; accountPkh: RawPkh; networkName: NetworkName } } - ) => { - state[payload.topic] = { accountPkh: payload.accountPkh, networkName: payload.networkName }; + togglePeerListUpdated: state => { + state.peerListUpdatedToggle = !state.peerListUpdatedToggle; + console.log("peerList is updated"); }, - - 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) - ) - ), }, });