From 0eaae57d1e75fbf596b8b65ff2340cd74bd4642d Mon Sep 17 00:00:00 2001 From: katspaugh Date: Thu, 14 Sep 2023 13:29:09 +0200 Subject: [PATCH 01/17] PoC: Safe Wallet provider --- src/safe-wallet-provider/provider.ts | 144 ++++++++++++++++++ .../useSafeWalletProvider.tsx | 92 +++++++++++ 2 files changed, 236 insertions(+) create mode 100644 src/safe-wallet-provider/provider.ts create mode 100644 src/safe-wallet-provider/useSafeWalletProvider.tsx diff --git a/src/safe-wallet-provider/provider.ts b/src/safe-wallet-provider/provider.ts new file mode 100644 index 0000000000..0d34c3d52c --- /dev/null +++ b/src/safe-wallet-provider/provider.ts @@ -0,0 +1,144 @@ +type SafeInfo = { + safeAddress: string + chainId: number +} + +type WalletSDK = { + signMessage: (message: string) => Promise<{ signature?: string }> + signTypedMessage: (typedData: unknown) => Promise<{ signature?: string }> + send: (params: { txs: unknown[]; params: { safeTxGas: number } }) => Promise<{ safeTxHash: string }> + getBySafeTxHash: (safeTxHash: string) => Promise<{ txHash?: string }> + proxy: (method: string, params: unknown[]) => Promise +} + +interface RpcRequest { + method: string + params?: unknown[] +} + +export type ProviderChainId = string + +export type ProviderAccounts = string[] + +export class SafeWalletProvider { + private readonly safe: SafeInfo + private readonly sdk: WalletSDK + private submittedTxs = new Map() + + constructor(safe: SafeInfo, sdk: WalletSDK) { + this.safe = safe + this.sdk = sdk + } + + async request(request: RpcRequest): Promise { + const { method, params = [] } = request + + switch (method) { + case 'eth_accounts': + return [this.safe.safeAddress] + + case 'net_version': + case 'eth_chainId': + return `0x${this.safe.chainId.toString(16)}` + + case 'personal_sign': { + const [message, address] = params as [string, string] + + if (this.safe.safeAddress.toLowerCase() !== address.toLowerCase()) { + throw new Error('The address or message hash is invalid') + } + + const response = await this.sdk.signMessage(message) + const signature = 'signature' in response ? response.signature : undefined + + return signature || '0x' + } + + case 'eth_sign': { + const [address, messageHash] = params as [string, string] + + if (this.safe.safeAddress.toLowerCase() !== address.toLowerCase() || !messageHash.startsWith('0x')) { + throw new Error('The address or message hash is invalid') + } + + const response = await this.sdk.signMessage(messageHash) + const signature = 'signature' in response ? response.signature : undefined + + return signature || '0x' + } + + case 'eth_signTypedData': + case 'eth_signTypedData_v4': { + const [address, typedData] = params as [string, unknown] + const parsedTypedData = typeof typedData === 'string' ? JSON.parse(typedData) : typedData + + if (this.safe.safeAddress.toLowerCase() !== address.toLowerCase()) { + throw new Error('The address is invalid') + } + + const response = await this.sdk.signTypedMessage(parsedTypedData) + const signature = 'signature' in response ? response.signature : undefined + return signature || '0x' + } + + case 'eth_sendTransaction': + const tx = { + value: '0', + data: '0x', + ...(params[0] as { gas: string | number; to: string }), + } + + // Some ethereum libraries might pass the gas as a hex-encoded string + // We need to convert it to a number because the SDK expects a number and our backend only supports + // Decimal numbers + if (typeof tx.gas === 'string' && tx.gas.startsWith('0x')) { + tx.gas = parseInt(tx.gas, 16) + } + + const resp = await this.sdk.send({ + txs: [tx], + params: { safeTxGas: Number(tx.gas) }, + }) + + // Store fake transaction + this.submittedTxs.set(resp.safeTxHash, { + from: this.safe.safeAddress, + hash: resp.safeTxHash, + gas: 0, + gasPrice: '0x00', + nonce: 0, + input: tx.data, + value: tx.value, + to: tx.to, + blockHash: null, + blockNumber: null, + transactionIndex: null, + }) + return resp.safeTxHash + + case 'eth_getTransactionByHash': + let txHash = params[0] as string + try { + const resp = await this.sdk.getBySafeTxHash(txHash) + txHash = resp.txHash || txHash + } catch (e) {} + // Use fake transaction if we don't have a real tx hash + if (this.submittedTxs.has(txHash)) { + return this.submittedTxs.get(txHash) + } + return await this.sdk.proxy(method, [txHash]) + + case 'eth_getTransactionReceipt': { + let txHash = params[0] as string + try { + const resp = await this.sdk.getBySafeTxHash(txHash) + txHash = resp.txHash || txHash + } catch (e) {} + return this.sdk.proxy(method, params) + } + + default: + return await this.sdk.proxy(method, params) + } + } +} diff --git a/src/safe-wallet-provider/useSafeWalletProvider.tsx b/src/safe-wallet-provider/useSafeWalletProvider.tsx new file mode 100644 index 0000000000..87c6369ded --- /dev/null +++ b/src/safe-wallet-provider/useSafeWalletProvider.tsx @@ -0,0 +1,92 @@ +import { useContext, useMemo } from 'react' +import useSafeInfo from '@/hooks/useSafeInfo' +import { SafeWalletProvider } from './provider' +import { TxModalContext } from '@/components/tx-flow' +import SignMessageFlow from '@/components/tx-flow/flows/SignMessage' +import { safeMsgSubscribe, SafeMsgEvent } from '@/services/safe-messages/safeMsgEvents' +import SafeAppsTxFlow from '@/components/tx-flow/flows/SafeAppsTx' +import { TxEvent, txSubscribe } from '@/services/tx/txEvents' +import type { BaseTransaction, EIP712TypedData } from '@safe-global/safe-apps-sdk' +import { useWeb3ReadOnly } from '@/hooks/wallets/web3' +import { getTransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' + +const useSafeWalletProvider = (): SafeWalletProvider | undefined => { + const { safe, safeAddress } = useSafeInfo() + const { chainId } = safe + const { setTxFlow } = useContext(TxModalContext) + const web3ReadOnly = useWeb3ReadOnly() + + const txFlowApi = useMemo(() => { + if (!safeAddress || !chainId) { + return + } + + return { + async signMessage(message: string | EIP712TypedData): Promise<{ signature: string }> { + const id = Math.random().toString(36).slice(2) + setTxFlow() + + return new Promise((resolve) => { + const unsubscribe = safeMsgSubscribe(SafeMsgEvent.SIGNATURE_PREPARED, ({ requestId, signature }) => { + if (requestId === id) { + resolve({ signature }) + unsubscribe() + } + }) + }) + }, + + async signTypedMessage(typedData: unknown): Promise<{ signature: string }> { + return this.signMessage(typedData as EIP712TypedData) + }, + + async send(params: { txs: unknown[]; params: { safeTxGas: number } }): Promise<{ safeTxHash: string }> { + const id = Math.random().toString(36).slice(2) + + setTxFlow( + , + ) + + return new Promise((resolve) => { + const unsubscribe = txSubscribe(TxEvent.SAFE_APPS_REQUEST, async ({ safeAppRequestId, safeTxHash }) => { + if (safeAppRequestId === id) { + resolve({ safeTxHash }) + unsubscribe() + } + }) + }) + }, + + async getBySafeTxHash(safeTxHash: string) { + return getTransactionDetails(chainId, safeTxHash) + }, + + async proxy(method: string, params: unknown[]) { + return web3ReadOnly?.send(method, params) + }, + } + }, [safeAddress, chainId, setTxFlow, web3ReadOnly]) + + return useMemo(() => { + if (!safeAddress || !chainId || !txFlowApi) { + return + } + + return new SafeWalletProvider( + { + safeAddress, + chainId: Number(chainId), + }, + txFlowApi, + ) + }, [safeAddress, chainId, txFlowApi]) +} + +export default useSafeWalletProvider From ac1314af1077848a552f35b35b58101c1c50c612 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Thu, 14 Sep 2023 13:47:50 +0200 Subject: [PATCH 02/17] Rm unused types --- src/safe-wallet-provider/provider.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/safe-wallet-provider/provider.ts b/src/safe-wallet-provider/provider.ts index 0d34c3d52c..bcc16e6ac1 100644 --- a/src/safe-wallet-provider/provider.ts +++ b/src/safe-wallet-provider/provider.ts @@ -16,10 +16,6 @@ interface RpcRequest { params?: unknown[] } -export type ProviderChainId = string - -export type ProviderAccounts = string[] - export class SafeWalletProvider { private readonly safe: SafeInfo private readonly sdk: WalletSDK From 71a3b5eb9c10e23cd458e9cca91dbe15530724eb Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 11 Sep 2023 13:24:44 +0200 Subject: [PATCH 03/17] feat: native wallet connect widget --- package.json | 3 + public/images/apps/wallet-connect.svg | 11 + src/components/common/Header/index.tsx | 7 + .../wallet-connect/hooks/useWalletConnect.ts | 313 ++++++++++++++++++ src/components/wallet-connect/index.tsx | 182 ++++++++++ .../wallet-connect/styles.module.css | 12 + src/config/constants.ts | 3 + yarn.lock | 121 ++++++- 8 files changed, 649 insertions(+), 3 deletions(-) create mode 100644 public/images/apps/wallet-connect.svg create mode 100644 src/components/wallet-connect/hooks/useWalletConnect.ts create mode 100644 src/components/wallet-connect/index.tsx create mode 100644 src/components/wallet-connect/styles.module.css diff --git a/package.json b/package.json index eea885d6c4..3732fd64f1 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,8 @@ "@sentry/react": "^7.28.1", "@sentry/tracing": "^7.28.1", "@truffle/hdwallet-provider": "^2.1.4", + "@walletconnect/core": "^2.10.0", + "@walletconnect/web3wallet": "^1.9.0", "@web3-onboard/coinbase": "^2.2.4", "@web3-onboard/core": "^2.21.0", "@web3-onboard/injected-wallets": "^2.10.0", @@ -110,6 +112,7 @@ "@types/react-qr-reader": "^2.1.4", "@types/semver": "^7.3.10", "@typescript-eslint/eslint-plugin": "^5.47.1", + "@walletconnect/types": "^2.10.0", "cross-env": "^7.0.3", "cypress": "^11.1.0", "cypress-file-upload": "^5.0.8", diff --git a/public/images/apps/wallet-connect.svg b/public/images/apps/wallet-connect.svg new file mode 100644 index 0000000000..495608a0bd --- /dev/null +++ b/public/images/apps/wallet-connect.svg @@ -0,0 +1,11 @@ + + + WalletConnect + Created with Sketch. + + + + + + + diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index f507662409..cef98e533d 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -15,6 +15,7 @@ import SafeLogo from '@/public/images/logo.svg' import Link from 'next/link' import useSafeAddress from '@/hooks/useSafeAddress' import BatchIndicator from '@/components/batch/BatchIndicator' +import { ConnectWC } from '@/components/wallet-connect' type HeaderProps = { onMenuToggle?: Dispatch> @@ -70,6 +71,12 @@ const Header = ({ onMenuToggle, onBatchToggle }: HeaderProps): ReactElement => { )} + {safeAddress && ( +
+ +
+ )} +
diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts new file mode 100644 index 0000000000..ad071db4f2 --- /dev/null +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -0,0 +1,313 @@ +import { useState, useCallback, useEffect } from 'react' +import type { SignClientTypes, SessionTypes } from '@walletconnect/types' +import { Core } from '@walletconnect/core' +import Web3WalletType, { Web3Wallet, Web3WalletTypes } from '@walletconnect/web3wallet' +import { IS_PRODUCTION, WALLETCONNECT_V2_PROJECT_ID } from '@/config/constants' +import { useCurrentChain } from '@/hooks/useChains' +import useSafeInfo from '@/hooks/useSafeInfo' + +const EVMBasedNamespaces: string = 'eip155' + +// see full list here: https://github.com/safe-global/safe-apps-sdk/blob/main/packages/safe-apps-provider/src/provider.ts#L35 +export const compatibleSafeMethods: string[] = [ + 'eth_accounts', + 'net_version', + 'eth_chainId', + 'personal_sign', + 'eth_sign', + 'eth_signTypedData', + 'eth_signTypedData_v4', + 'eth_sendTransaction', + 'eth_blockNumber', + 'eth_getBalance', + 'eth_getCode', + 'eth_getTransactionCount', + 'eth_getStorageAt', + 'eth_getBlockByNumber', + 'eth_getBlockByHash', + 'eth_getTransactionByHash', + 'eth_getTransactionReceipt', + 'eth_estimateGas', + 'eth_call', + 'eth_getLogs', + 'eth_gasPrice', + 'wallet_getPermissions', + 'wallet_requestPermissions', + 'safe_setSettings', +] + +// see https://docs.walletconnect.com/2.0/specs/sign/error-codes +const UNSUPPORTED_CHAIN_ERROR_CODE = 5100 +const INVALID_METHOD_ERROR_CODE = 1001 +const USER_REJECTED_REQUEST_CODE = 4001 +const USER_DISCONNECTED_CODE = 6000 + +const logger = IS_PRODUCTION ? undefined : 'debug' + +export const errorLabel = + 'We were unable to create a connection due to compatibility issues with the latest WalletConnect v2 upgrade. We are actively working with the WalletConnect team and the dApps to get these issues resolved. Use Safe Apps instead wherever possible.' + +export type wcConnectType = (uri: string) => Promise +export type wcDisconnectType = () => Promise + +export type useWalletConnectType = { + wcClientData: SignClientTypes.Metadata | undefined + wcConnect: wcConnectType + wcDisconnect: wcDisconnectType + wcApproveSession: () => Promise + isWallectConnectInitialized: boolean + error: string | undefined + sessionProposal: Web3WalletTypes.SessionProposal | undefined + wcState: WC_CONNECT_STATE +} + +// MOVE TO CONSTANTS +export const SAFE_WALLET_METADATA = { + name: 'Safe Wallet', + description: 'The most trusted platform to manage digital assets on Ethereum', + url: 'https://app.safe.global', + icons: ['https://app.safe.global/favicons/mstile-150x150.png', 'https://app.safe.global/favicons/logo_120x120.png'], +} + +export enum WC_CONNECT_STATE { + NOT_CONNECTED, + PAIRING_SESSION, + PENDING_SESSION_REQUEST, + APPROVING_SESSION, + REJECTING_SESSION, + CONNECTED, +} + +const useWalletConnect = (): useWalletConnectType => { + const [web3wallet, setWeb3wallet] = useState() + const [sessionProposal, setSessionProposal] = useState() + const [wcState, setWcState] = useState(WC_CONNECT_STATE.NOT_CONNECTED) + const [wcSession, setWcSession] = useState() + const [isWallectConnectInitialized, setIsWallectConnectInitialized] = useState(false) + const [error, setError] = useState() + + const chainInfo = useCurrentChain() + const { safe } = useSafeInfo() + + // Initializing v2, see https://docs.walletconnect.com/2.0/javascript/web3wallet/wallet-usage + useEffect(() => { + const initializeWalletConnectV2Client = async () => { + const core = new Core({ + projectId: WALLETCONNECT_V2_PROJECT_ID, + logger, + }) + + const web3wallet = await Web3Wallet.init({ + core, + metadata: SAFE_WALLET_METADATA, + }) + + setWeb3wallet(web3wallet) + } + + try { + initializeWalletConnectV2Client() + } catch (error) { + console.log('Error on walletconnect version 2 initialization: ', error) + setIsWallectConnectInitialized(true) + } + }, []) + + // session_request needs to be a separate Effect because a valid wcSession should be present + useEffect(() => { + if (!isWallectConnectInitialized || !web3wallet || !wcSession) { + return + } + web3wallet.on('session_request', async (event) => { + const { topic, id } = event + const { request, chainId: transactionChainId } = event.params + const { method, params } = request + + const isSafeChainId = transactionChainId === `${EVMBasedNamespaces}:${safe.chainId}` + + // we only accept transactions from the Safe chain + if (!isSafeChainId) { + const errorMessage = `Transaction rejected: the connected Dapp is not set to the correct chain. Make sure the Dapp only uses ${chainInfo?.chainName} to interact with this Safe.` + setError(errorMessage) + await web3wallet.respondSessionRequest({ + topic, + response: rejectResponse(id, UNSUPPORTED_CHAIN_ERROR_CODE, errorMessage), + }) + return + } + + try { + setError(undefined) + // Handle request + /* const result = await web3Provider.send(method, params) + await web3wallet.respondSessionRequest({ + topic, + response: { + id, + jsonrpc: '2.0', + result, + }, + }) + */ + // TODO TRACKING + // trackEvent(TRANSACTION_CONFIRMED_ACTION, WALLET_CONNECT_VERSION_2, wcSession.peer.metadata) + } catch (error: any) { + setError(error?.message) + const isUserRejection = error?.message?.includes?.('Transaction was rejected') + const code = isUserRejection ? USER_REJECTED_REQUEST_CODE : INVALID_METHOD_ERROR_CODE + await web3wallet.respondSessionRequest({ + topic, + response: rejectResponse(id, code, error.message), + }) + } + }) + }, [chainInfo, wcSession, isWallectConnectInitialized, web3wallet, safe]) + + // we set here the events & restore an active previous session + useEffect(() => { + if (!isWallectConnectInitialized && web3wallet) { + // we try to find a compatible active session + const activeSessions = web3wallet.getActiveSessions() + const compatibleSession = Object.keys(activeSessions) + .map((topic) => activeSessions[topic]) + .find( + (session) => + session.namespaces[EVMBasedNamespaces].accounts[0] === + `${EVMBasedNamespaces}:${safe.chainId}:${safe.address.value}`, // Safe Account + ) + + if (compatibleSession) { + setWcSession(compatibleSession) + } + + // events + web3wallet.on('session_proposal', async (proposal) => { + setSessionProposal(proposal) + setWcState(WC_CONNECT_STATE.PENDING_SESSION_REQUEST) + }) + + web3wallet.on('session_delete', async () => { + setWcState(WC_CONNECT_STATE.NOT_CONNECTED) + setWcSession(undefined) + setError(undefined) + }) + + setIsWallectConnectInitialized(true) + } + }, [safe, web3wallet, isWallectConnectInitialized, chainInfo]) + + const wcConnect = useCallback( + async (uri: string) => { + const isValidWalletConnectUri = uri && uri.startsWith('wc') + + if (isValidWalletConnectUri && web3wallet) { + setWcState(WC_CONNECT_STATE.PAIRING_SESSION) + await web3wallet.core.pairing.pair({ uri }) + } + }, + [web3wallet], + ) + + const wcApproveSession = useCallback(async () => { + if (!sessionProposal || !web3wallet) { + throw new Error('Cannot approve session without pending session proposal') + } + + const { id, params } = sessionProposal + const { requiredNamespaces } = params + const requiredNamespace = requiredNamespaces[EVMBasedNamespaces] + + const safeChain = `${EVMBasedNamespaces}:${safe.chainId}` + const safeEvents = requiredNamespace?.events || [] // we accept all events like chainChanged & accountsChanged (even if they are not compatible with the Safe) + + const requiredChains = [...(requiredNamespace.chains ?? [])] + // If the user accepts we always return all required namespaces and add the safe chain to it + const safeAccount = `${EVMBasedNamespaces}:${safe.chainId}:${safe.address.value}` + + // We first fake that our Safe is available on all required networks + const safeOnRequiredChains = requiredChains.map( + (requiredChain) => `${requiredChains[0] ?? safeChain}:${safe.address.value}`, + ) + const wcSession = await web3wallet.approveSession({ + id, + namespaces: { + eip155: { + accounts: safeOnRequiredChains.includes(safeAccount) + ? safeOnRequiredChains + : [...safeOnRequiredChains, safeAccount], // only the Safe account + chains: requiredChains, // only the Safe chain + methods: compatibleSafeMethods, // only the Safe methods + events: safeEvents, + }, + }, + }) + + // Then we update the session and reduce the Safe to the requested network only + if (!safeOnRequiredChains.includes(safeAccount) || safeOnRequiredChains.length > 1) { + if (!requiredChains.includes(safeChain)) { + requiredChains.push(safeChain) + } + + // Emit accountsChanged and chainChanged event + await web3wallet.updateSession({ + topic: wcSession.topic, + namespaces: { + eip155: { + accounts: [safeAccount], + chains: requiredChains, + methods: compatibleSafeMethods, + events: safeEvents, + }, + }, + }) + } + + // + setWcSession(wcSession) + setSessionProposal(undefined) + setError(undefined) + setWcState(WC_CONNECT_STATE.CONNECTED) + }, [safe.address, safe.chainId, sessionProposal, web3wallet]) + + const wcDisconnect = useCallback(async () => { + if (wcSession && web3wallet) { + await web3wallet.disconnectSession({ + topic: wcSession.topic, + reason: { + code: USER_DISCONNECTED_CODE, + message: 'User disconnected. Safe Wallet Session ended by the user', + }, + }) + setWcState(WC_CONNECT_STATE.NOT_CONNECTED) + + setWcSession(undefined) + setError(undefined) + } + }, [web3wallet, wcSession]) + + const wcClientData = wcSession?.peer.metadata + + return { + wcConnect, + wcClientData, + wcDisconnect, + wcApproveSession, + isWallectConnectInitialized, + error, + wcState, + sessionProposal, + } +} + +export default useWalletConnect + +const rejectResponse = (id: number, code: number, message: string) => { + return { + id, + jsonrpc: '2.0', + error: { + code, + message, + }, + } +} diff --git a/src/components/wallet-connect/index.tsx b/src/components/wallet-connect/index.tsx new file mode 100644 index 0000000000..11b13d12ec --- /dev/null +++ b/src/components/wallet-connect/index.tsx @@ -0,0 +1,182 @@ +import { Box, Button, IconButton, Paper, Popover, SvgIcon, TextField, Typography } from '@mui/material' +import WcIcon from '@/public/images/apps/wallet-connect.svg' +import { type ChangeEvent, useCallback, useState, useRef } from 'react' +import useWalletConnect, { WC_CONNECT_STATE } from './hooks/useWalletConnect' +import css from './styles.module.css' +import session from '@/services/local-storage/session' +import type { Web3WalletTypes } from '@walletconnect/web3wallet' +import UnreadBadge from '../common/UnreadBadge' + +const EVMBasedNamespaces: string = 'eip155' + +const extractInformationFromProposal = (sessionProposal: Web3WalletTypes.SessionProposal | undefined) => { + if (!sessionProposal) { + return undefined + } + + const { origin, validation } = sessionProposal.verifyContext.verified + const requiredNamespaces = sessionProposal.params.requiredNamespaces[EVMBasedNamespaces] + + return { requiredNamespaces, origin, validation } +} + +export const ConnectWC = () => { + const [openModal, setOpenModal] = useState(false) + const [wcConnectUrl, setWcConnectUrl] = useState('') + const [isConnecting, setIsConnecting] = useState(false) + const { wcConnect, wcClientData, wcApproveSession, wcState, sessionProposal } = useWalletConnect() + const anchorElem = useRef(null) + + const isConnected = !!wcClientData + + const requiredChains = sessionProposal ? session : [] + + const proposalInfo = extractInformationFromProposal(sessionProposal) + + console.log('WcClientData', wcClientData) + + const handleWidgetIconClick = () => { + setOpenModal((prev) => !prev) + } + + const onConnect = useCallback( + async (uri: string) => { + await wcConnect(uri) + setIsConnecting(false) + }, + [wcConnect], + ) + + const onChangeWcUrl = (event: ChangeEvent) => { + const newValue = event.target.value + setWcConnectUrl(newValue) + } + + const onPaste = useCallback( + (event: React.ClipboardEvent) => { + const connectWithUri = (data: string) => { + if (data.startsWith('wc')) { + setIsConnecting(true) + onConnect(data) + } + } + + setWcConnectUrl('') + + if (wcClientData) { + return + } + + const items = event.clipboardData.items + + for (const index in items) { + const item = items[index] + + if (item.kind === 'string' && item.type === 'text/plain') { + connectWithUri(event.clipboardData.getData('Text')) + } + } + }, + [wcClientData, onConnect], + ) + + return ( + <> + + + + + + setOpenModal(false)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + sx={{ mt: 1 }} + > + +
+
+ + Connect via Wallet Connect + +
+
+ {WC_CONNECT_STATE.PENDING_SESSION_REQUEST === wcState ? ( + + + Session proposal + +
    +
  • + + Request from {sessionProposal?.verifyContext.verified.origin} + +
  • +
  • + + Required chains {proposalInfo?.requiredNamespaces.chains?.join(', ')} + +
  • +
  • + + Required methods {proposalInfo?.requiredNamespaces.methods?.join(', ')} + +
  • +
  • + + Required events {proposalInfo?.requiredNamespaces.events?.join(', ')} + +
  • +
+ + + + + +
+ ) : WC_CONNECT_STATE.CONNECTED === wcState && wcClientData ? ( + + + Successfully connected + + + + Connected to {wcClientData.name} + + App logo + + + ) : ( + + Paste the WC URL to connect with wc + + + )} +
+
+ + ) +} diff --git a/src/components/wallet-connect/styles.module.css b/src/components/wallet-connect/styles.module.css new file mode 100644 index 0000000000..f69420563d --- /dev/null +++ b/src/components/wallet-connect/styles.module.css @@ -0,0 +1,12 @@ +.wrapper { + width: 446px; + border: 1px solid var(--color-border-light); +} + +.popoverHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-3); + border-bottom: 2px solid var(--color-background-main); +} diff --git a/src/config/constants.ts b/src/config/constants.ts index 577265e35f..d20382e40f 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -97,3 +97,6 @@ export const IS_OFFICIAL_HOST = process.env.NEXT_PUBLIC_IS_OFFICIAL_HOST || fals export const REDEFINE_SIMULATION_URL = 'https://dashboard.redefine.net/reports/' export const REDEFINE_API = process.env.NEXT_PUBLIC_REDEFINE_API export const REDEFINE_ARTICLE = 'https://safe.mirror.xyz/rInLWZwD_sf7enjoFerj6FIzCYmVMGrrV8Nhg4THdwI' + +// Wallet Connect v2 +export const WALLETCONNECT_V2_PROJECT_ID = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID diff --git a/yarn.lock b/yarn.lock index 22db38c51a..966326124d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3604,7 +3604,7 @@ "@stablelib/binary" "^1.0.1" "@stablelib/wipe" "^1.0.1" -"@stablelib/sha256@1.0.1": +"@stablelib/sha256@1.0.1", "@stablelib/sha256@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/sha256/-/sha256-1.0.1.tgz#77b6675b67f9b0ea081d2e31bda4866297a3ae4f" integrity sha512-GIIH3e6KH+91FqGV42Kcj71Uefd/QEe7Dy42sBTeqppXV95ggCcxLTk39bEr+lZfJmp+ghsR07J++ORkRELsBQ== @@ -4566,6 +4566,25 @@ resolved "https://registry.yarnpkg.com/@wagmi/chains/-/chains-1.7.0.tgz#8f6ad81cf867e1788417f7c978ca92bc083ecaf6" integrity sha512-TKVeHv0GqP5sV1yQ8BDGYToAFezPnCexbbBpeH14x7ywi5a1dDStPffpt9x+ytE6LJWkZ6pAMs/HNWXBQ5Nqmw== +"@walletconnect/auth-client@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@walletconnect/auth-client/-/auth-client-2.1.1.tgz#45548fc5d5e5ac155503d1b42ac97a96a2cba98d" + integrity sha512-rFGBG3pLkmwCc5DcL9JRCsvOAmPjUcHGxm+KlX31yXNOT1QACT8Gyd8ODSOmtvz5CXZS5dPWBuvO03LUSRbPkw== + dependencies: + "@ethersproject/hash" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@stablelib/random" "^1.0.2" + "@stablelib/sha256" "^1.0.1" + "@walletconnect/core" "^2.9.0" + "@walletconnect/events" "^1.0.1" + "@walletconnect/heartbeat" "^1.2.1" + "@walletconnect/jsonrpc-utils" "^1.0.8" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/time" "^1.0.2" + "@walletconnect/utils" "^2.9.0" + events "^3.3.0" + isomorphic-unfetch "^3.1.0" + "@walletconnect/browser-utils@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.8.0.tgz#33c10e777aa6be86c713095b5206d63d32df0951" @@ -4587,6 +4606,28 @@ "@walletconnect/types" "^1.8.0" "@walletconnect/utils" "^1.8.0" +"@walletconnect/core@2.10.0", "@walletconnect/core@^2.10.0", "@walletconnect/core@^2.9.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.10.0.tgz#b659de4dfb374becd938964abd4f2150d410e617" + integrity sha512-Z8pdorfIMueuiBXLdnf7yloiO9JIiobuxN3j0OTal+MYc4q5/2O7d+jdD1DAXbLi1taJx3x60UXT/FPVkjIqIQ== + dependencies: + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.13" + "@walletconnect/keyvaluestorage" "^1.0.2" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/relay-api" "^1.0.9" + "@walletconnect/relay-auth" "^1.0.4" + "@walletconnect/safe-json" "^1.0.2" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.0" + "@walletconnect/utils" "2.10.0" + events "^3.3.0" + lodash.isequal "4.5.0" + uint8arrays "^3.1.0" + "@walletconnect/core@2.9.2": version "2.9.2" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.9.2.tgz#c46734ca63771b28fd77606fd521930b7ecfc5e1" @@ -4669,7 +4710,7 @@ keyvaluestorage-interface "^1.0.0" tslib "1.14.1" -"@walletconnect/heartbeat@1.2.1": +"@walletconnect/heartbeat@1.2.1", "@walletconnect/heartbeat@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@walletconnect/heartbeat/-/heartbeat-1.2.1.tgz#afaa3a53232ae182d7c9cff41c1084472d8f32e9" integrity sha512-yVzws616xsDLJxuG/28FqtZ5rzrTA4gUjdEMTbWB5Y8V1XHRmqq4efAxCw5ie7WjbXFSUyBHaWlMR+2/CpQC5Q== @@ -4742,7 +4783,7 @@ safe-json-utils "^1.1.1" tslib "1.14.1" -"@walletconnect/logger@^2.0.1": +"@walletconnect/logger@2.0.1", "@walletconnect/logger@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.0.1.tgz#7f489b96e9a1ff6bf3e58f0fbd6d69718bf844a8" integrity sha512-SsTKdsgWm+oDTBeNE/zHxxr5eJfZmE9/5yp/Ku+zJtcTAjELb3DXueWkDXmE9h8uHIbJzIb5wj5lPdzyrjT6hQ== @@ -4834,6 +4875,21 @@ dependencies: tslib "1.14.1" +"@walletconnect/sign-client@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.10.0.tgz#0fee8f12821e37783099f0c7bd64e6efdfbd9d86" + integrity sha512-hbDljDS53kR/It3oXD91UkcOsT6diNnW5+Zzksm0YEfwww5dop/YfNlcdnc8+jKUhWOL/YDPNQCjzsCSNlVzbw== + dependencies: + "@walletconnect/core" "2.10.0" + "@walletconnect/events" "^1.0.1" + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.0" + "@walletconnect/utils" "2.10.0" + events "^3.3.0" + "@walletconnect/sign-client@2.9.2": version "2.9.2" resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.9.2.tgz#ff4c81c082c2078878367d07f24bcb20b1f7ab9e" @@ -4865,6 +4921,18 @@ dependencies: tslib "1.14.1" +"@walletconnect/types@2.10.0", "@walletconnect/types@^2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.10.0.tgz#5d63235b49e03d609521402a4b49627dbc4ed514" + integrity sha512-kSTA/WZnbKdEbvbXSW16Ty6dOSzOZCHnGg6JH7q1MuraalD2HuNg00lVVu7QAZ/Rj1Gn9DAkrgP5Wd5a8Xq//Q== + dependencies: + "@walletconnect/events" "^1.0.1" + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/keyvaluestorage" "^1.0.2" + "@walletconnect/logger" "^2.0.1" + events "^3.3.0" + "@walletconnect/types@2.9.2": version "2.9.2" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.9.2.tgz#d5fd5a61dc0f41cbdca59d1885b85207ac7bf8c5" @@ -4897,6 +4965,26 @@ "@walletconnect/utils" "2.9.2" events "^3.3.0" +"@walletconnect/utils@2.10.0", "@walletconnect/utils@^2.9.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.10.0.tgz#6918d12180d797b8bd4a19fb2ff128e394e181d6" + integrity sha512-9GRyEz/7CJW+G04RvrjPET5k7hOEsB9b3fF9cWDk/iDCxSWpbkU/hv/urRB36C+gvQMAZgIZYX3dHfzJWkY/2g== + dependencies: + "@stablelib/chacha20poly1305" "1.0.1" + "@stablelib/hkdf" "1.0.1" + "@stablelib/random" "^1.0.2" + "@stablelib/sha256" "1.0.1" + "@stablelib/x25519" "^1.0.3" + "@walletconnect/relay-api" "^1.0.9" + "@walletconnect/safe-json" "^1.0.2" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.0" + "@walletconnect/window-getters" "^1.0.1" + "@walletconnect/window-metadata" "^1.0.1" + detect-browser "5.3.0" + query-string "7.1.3" + uint8arrays "^3.1.0" + "@walletconnect/utils@2.9.2": version "2.9.2" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.9.2.tgz#035bdb859ee81a4bcc6420f56114cc5ec3e30afb" @@ -4930,6 +5018,20 @@ js-sha3 "0.8.0" query-string "6.13.5" +"@walletconnect/web3wallet@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.9.0.tgz#ad4094e1e2ed757bc75efa961121b66b2eeb4306" + integrity sha512-3uu6GbOz2uwcmVaIpijkPlReywC1GsFtwJOB1bJZOkc8wjtNmR3jUMwqxWUv8ojbmDVVWQl1HN7Sptkrmq9Xyw== + dependencies: + "@walletconnect/auth-client" "2.1.1" + "@walletconnect/core" "2.10.0" + "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "2.0.1" + "@walletconnect/sign-client" "2.10.0" + "@walletconnect/types" "2.10.0" + "@walletconnect/utils" "2.10.0" + "@walletconnect/window-getters@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.0.tgz#1053224f77e725dfd611c83931b5f6c98c32bfc8" @@ -9571,6 +9673,14 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isomorphic-unfetch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" + integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== + dependencies: + node-fetch "^2.6.1" + unfetch "^4.2.0" + isomorphic-ws@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" @@ -13923,6 +14033,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" From d0c947703e9b5ad79093afda0911966d9837c6fc Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 11 Sep 2023 13:35:31 +0200 Subject: [PATCH 04/17] feat: show dapp icon in header --- .../wallet-connect/hooks/useWalletConnect.ts | 1 + src/components/wallet-connect/index.tsx | 17 ++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index ad071db4f2..359f085acb 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -178,6 +178,7 @@ const useWalletConnect = (): useWalletConnectType => { if (compatibleSession) { setWcSession(compatibleSession) + setWcState(WC_CONNECT_STATE.CONNECTED) } // events diff --git a/src/components/wallet-connect/index.tsx b/src/components/wallet-connect/index.tsx index 11b13d12ec..e355f39ee1 100644 --- a/src/components/wallet-connect/index.tsx +++ b/src/components/wallet-connect/index.tsx @@ -83,15 +83,14 @@ export const ConnectWC = () => { return ( <> - - - + + App logo Date: Mon, 11 Sep 2023 16:23:25 +0200 Subject: [PATCH 05/17] feat: give user option to proceed with wrong network --- .../wallet-connect/hooks/useWalletConnect.ts | 39 +++-- src/components/wallet-connect/index.tsx | 151 ++++++++++++------ 2 files changed, 133 insertions(+), 57 deletions(-) diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index 359f085acb..7d74a1fc83 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -1,7 +1,8 @@ import { useState, useCallback, useEffect } from 'react' import type { SignClientTypes, SessionTypes } from '@walletconnect/types' import { Core } from '@walletconnect/core' -import Web3WalletType, { Web3Wallet, Web3WalletTypes } from '@walletconnect/web3wallet' +import type Web3WalletType from '@walletconnect/web3wallet' +import { Web3Wallet, type Web3WalletTypes } from '@walletconnect/web3wallet' import { IS_PRODUCTION, WALLETCONNECT_V2_PROJECT_ID } from '@/config/constants' import { useCurrentChain } from '@/hooks/useChains' import useSafeInfo from '@/hooks/useSafeInfo' @@ -59,6 +60,7 @@ export type useWalletConnectType = { error: string | undefined sessionProposal: Web3WalletTypes.SessionProposal | undefined wcState: WC_CONNECT_STATE + acceptInvalidSession: () => void } // MOVE TO CONSTANTS @@ -74,6 +76,7 @@ export enum WC_CONNECT_STATE { PAIRING_SESSION, PENDING_SESSION_REQUEST, APPROVING_SESSION, + APPROVE_INVALID_SESSION, REJECTING_SESSION, CONNECTED, } @@ -214,6 +217,7 @@ const useWalletConnect = (): useWalletConnectType => { throw new Error('Cannot approve session without pending session proposal') } + console.log('Approving session', sessionProposal) const { id, params } = sessionProposal const { requiredNamespaces } = params const requiredNamespace = requiredNamespaces[EVMBasedNamespaces] @@ -250,17 +254,23 @@ const useWalletConnect = (): useWalletConnectType => { } // Emit accountsChanged and chainChanged event - await web3wallet.updateSession({ - topic: wcSession.topic, - namespaces: { - eip155: { - accounts: [safeAccount], - chains: requiredChains, - methods: compatibleSafeMethods, - events: safeEvents, + try { + await web3wallet.updateSession({ + topic: wcSession.topic, + namespaces: { + eip155: { + accounts: [safeAccount], + chains: requiredChains, + methods: compatibleSafeMethods, + events: safeEvents, + }, }, - }, - }) + }) + } catch (error) { + setWcState(WC_CONNECT_STATE.APPROVE_INVALID_SESSION) + setWcSession(wcSession) + return + } } // @@ -270,6 +280,12 @@ const useWalletConnect = (): useWalletConnectType => { setWcState(WC_CONNECT_STATE.CONNECTED) }, [safe.address, safe.chainId, sessionProposal, web3wallet]) + const acceptInvalidSession = useCallback(() => { + setWcState(WC_CONNECT_STATE.CONNECTED) + setError(undefined) + setSessionProposal(undefined) + }, []) + const wcDisconnect = useCallback(async () => { if (wcSession && web3wallet) { await web3wallet.disconnectSession({ @@ -297,6 +313,7 @@ const useWalletConnect = (): useWalletConnectType => { error, wcState, sessionProposal, + acceptInvalidSession, } } diff --git a/src/components/wallet-connect/index.tsx b/src/components/wallet-connect/index.tsx index e355f39ee1..0ea160a178 100644 --- a/src/components/wallet-connect/index.tsx +++ b/src/components/wallet-connect/index.tsx @@ -1,11 +1,26 @@ -import { Box, Button, IconButton, Paper, Popover, SvgIcon, TextField, Typography } from '@mui/material' +import { + Alert, + AlertTitle, + Box, + Button, + Card, + IconButton, + Paper, + Popover, + SvgIcon, + TextField, + Tooltip, + Typography, +} from '@mui/material' import WcIcon from '@/public/images/apps/wallet-connect.svg' import { type ChangeEvent, useCallback, useState, useRef } from 'react' import useWalletConnect, { WC_CONNECT_STATE } from './hooks/useWalletConnect' import css from './styles.module.css' -import session from '@/services/local-storage/session' import type { Web3WalletTypes } from '@walletconnect/web3wallet' -import UnreadBadge from '../common/UnreadBadge' +import LogoutIcon from '@mui/icons-material/Logout' +import useChains from '@/hooks/useChains' +import ChainIndicator from '../common/ChainIndicator' +import useSafeInfo from '@/hooks/useSafeInfo' const EVMBasedNamespaces: string = 'eip155' @@ -15,24 +30,32 @@ const extractInformationFromProposal = (sessionProposal: Web3WalletTypes.Session } const { origin, validation } = sessionProposal.verifyContext.verified + const metadata = sessionProposal.params.proposer.metadata const requiredNamespaces = sessionProposal.params.requiredNamespaces[EVMBasedNamespaces] - return { requiredNamespaces, origin, validation } + return { requiredNamespaces, origin, validation, metadata } } export const ConnectWC = () => { const [openModal, setOpenModal] = useState(false) const [wcConnectUrl, setWcConnectUrl] = useState('') const [isConnecting, setIsConnecting] = useState(false) - const { wcConnect, wcClientData, wcApproveSession, wcState, sessionProposal } = useWalletConnect() + const { wcConnect, wcDisconnect, wcClientData, wcApproveSession, acceptInvalidSession, wcState, sessionProposal } = + useWalletConnect() const anchorElem = useRef(null) + const chains = useChains() + const { safe } = useSafeInfo() const isConnected = !!wcClientData - const requiredChains = sessionProposal ? session : [] - const proposalInfo = extractInformationFromProposal(sessionProposal) + const unsupportedChains = proposalInfo?.requiredNamespaces.chains?.find( + (chain) => safe.chainId !== chain.slice(EVMBasedNamespaces.length + 1), + ) + + console.log('Unsupported chains', unsupportedChains) + console.log('WcClientData', wcClientData) const handleWidgetIconClick = () => { @@ -84,13 +107,21 @@ export const ConnectWC = () => { <> - App logo + {wcClientData?.icons[0] && ( + App logo + )} { - {WC_CONNECT_STATE.PENDING_SESSION_REQUEST === wcState ? ( + {WC_CONNECT_STATE.PENDING_SESSION_REQUEST === wcState && sessionProposal ? ( - - Session proposal - -
    -
  • - - Request from {sessionProposal?.verifyContext.verified.origin} - -
  • -
  • - - Required chains {proposalInfo?.requiredNamespaces.chains?.join(', ')} - -
  • -
  • - - Required methods {proposalInfo?.requiredNamespaces.methods?.join(', ')} + + AppLogo + + {proposalInfo?.metadata.name} wants to connect. + + + {sessionProposal.verifyContext.verified.origin} + + + `1px solid ${palette.border.light}` }}> + + + Requested permissions -
  • -
  • - - Required events {proposalInfo?.requiredNamespaces.events?.join(', ')} + {proposalInfo?.requiredNamespaces.methods.map((method) => ( + {method} + ))} + + + `1px solid ${palette.border.light}` }}> + + + Requested chains -
  • -
+ {proposalInfo?.requiredNamespaces.chains?.map((chain) => { + const chainWithoutPrefix = chain.slice(EVMBasedNamespaces.length + 1) + const chainConfig = chains.configs.find((config) => config.chainId === chainWithoutPrefix) + return + })} +
+ + {unsupportedChains && unsupportedChains.length > 0 && ( + + Unsupported networks requested + The dApp requested networks that different from your opened Safe. + + )} + ) : WC_CONNECT_STATE.APPROVE_INVALID_SESSION === wcState ? ( + + + Mismatching network + The dApp did not accept the Safe's network. Do you still want to proceed? + + + + + + ) : WC_CONNECT_STATE.CONNECTED === wcState && wcClientData ? ( - - Successfully connected - - - - Connected to {wcClientData.name} - - App logo + + App logo + {wcClientData.name} + + + + + + ) : ( From ccaa9121e291ebc7e869f27d7959ab62d3fed2ee Mon Sep 17 00:00:00 2001 From: schmanu Date: Thu, 14 Sep 2023 09:32:12 +0200 Subject: [PATCH 06/17] fix: approving sessions --- .../wallet-connect/hooks/useWalletConnect.ts | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index 7d74a1fc83..26cf1cbb3e 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -231,21 +231,38 @@ const useWalletConnect = (): useWalletConnectType => { // We first fake that our Safe is available on all required networks const safeOnRequiredChains = requiredChains.map( - (requiredChain) => `${requiredChains[0] ?? safeChain}:${safe.address.value}`, + (requiredChain) => `${requiredChain ?? safeChain}:${safe.address.value}`, ) - const wcSession = await web3wallet.approveSession({ - id, - namespaces: { - eip155: { - accounts: safeOnRequiredChains.includes(safeAccount) - ? safeOnRequiredChains - : [...safeOnRequiredChains, safeAccount], // only the Safe account - chains: requiredChains, // only the Safe chain - methods: compatibleSafeMethods, // only the Safe methods - events: safeEvents, + + console.log('Approving these safe addresses: ', safeOnRequiredChains) + let wcSession: SessionTypes.Struct + try { + wcSession = await web3wallet.approveSession({ + id, + namespaces: { + eip155: { + accounts: [safeAccount], // only the Safe account + chains: [safeChain], // only the Safe chain + methods: compatibleSafeMethods, // only the Safe methods + events: safeEvents, + }, }, - }, - }) + }) + } catch (error) { + wcSession = await web3wallet.approveSession({ + id, + namespaces: { + eip155: { + accounts: safeOnRequiredChains.includes(safeAccount) + ? safeOnRequiredChains + : [...safeOnRequiredChains, safeAccount], // Add all required chains on top + chains: requiredChains, // return the required Safes + methods: compatibleSafeMethods, // only the Safe methods + events: safeEvents, + }, + }, + }) + } // Then we update the session and reduce the Safe to the requested network only if (!safeOnRequiredChains.includes(safeAccount) || safeOnRequiredChains.length > 1) { From 34c9fbcf9f0c2324eb8a4be77346483eabb38dd7 Mon Sep 17 00:00:00 2001 From: schmanu Date: Thu, 14 Sep 2023 14:16:22 +0200 Subject: [PATCH 07/17] fix: type conflict --- package.json | 4 ++-- .../wallet-connect/hooks/useWalletConnect.ts | 3 +-- yarn.lock | 14 +++++++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3732fd64f1..da92337e10 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,6 @@ "@types/react-qr-reader": "^2.1.4", "@types/semver": "^7.3.10", "@typescript-eslint/eslint-plugin": "^5.47.1", - "@walletconnect/types": "^2.10.0", "cross-env": "^7.0.3", "cypress": "^11.1.0", "cypress-file-upload": "^5.0.8", @@ -129,6 +128,7 @@ "ts-prune": "^0.10.3", "typechain": "^8.0.0", "typescript": "4.9.4", - "typescript-plugin-css-modules": "^4.2.2" + "typescript-plugin-css-modules": "^4.2.2", + "walletconnect-v2-types": "npm:@walletconnect/types@^2.10.0" } } diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index 26cf1cbb3e..e46605c7da 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect } from 'react' -import type { SignClientTypes, SessionTypes } from '@walletconnect/types' +import type { SignClientTypes, SessionTypes } from 'walletconnect-v2-types' import { Core } from '@walletconnect/core' import type Web3WalletType from '@walletconnect/web3wallet' import { Web3Wallet, type Web3WalletTypes } from '@walletconnect/web3wallet' @@ -234,7 +234,6 @@ const useWalletConnect = (): useWalletConnectType => { (requiredChain) => `${requiredChain ?? safeChain}:${safe.address.value}`, ) - console.log('Approving these safe addresses: ', safeOnRequiredChains) let wcSession: SessionTypes.Struct try { wcSession = await web3wallet.approveSession({ diff --git a/yarn.lock b/yarn.lock index 966326124d..a09d574aba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4921,7 +4921,7 @@ dependencies: tslib "1.14.1" -"@walletconnect/types@2.10.0", "@walletconnect/types@^2.10.0": +"@walletconnect/types@2.10.0": version "2.10.0" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.10.0.tgz#5d63235b49e03d609521402a4b49627dbc4ed514" integrity sha512-kSTA/WZnbKdEbvbXSW16Ty6dOSzOZCHnGg6JH7q1MuraalD2HuNg00lVVu7QAZ/Rj1Gn9DAkrgP5Wd5a8Xq//Q== @@ -14271,6 +14271,18 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +"walletconnect-v2-types@npm:@walletconnect/types@^2.10.0": + version "2.10.1" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.10.1.tgz#1355bce236f3eef575716ea3efe4beed98a873ef" + integrity sha512-7pccAhajQdiH2kYywjE1XI64IqRI+4ioyGy0wvz8d0UFQ/DSG3MLKR8jHf5aTOafQQ/HRLz6xvlzN4a7gIVkUQ== + dependencies: + "@walletconnect/events" "^1.0.1" + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/keyvaluestorage" "^1.0.2" + "@walletconnect/logger" "^2.0.1" + events "^3.3.0" + warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" From 7d88aa115b7eca78f17df90f34c42c0d144a85aa Mon Sep 17 00:00:00 2001 From: schmanu Date: Thu, 14 Sep 2023 18:39:52 +0200 Subject: [PATCH 08/17] feat: add signing flow to new wc widget --- .../wallet-connect/hooks/WalletConnect.ts | 259 +++++++++++++++++ .../wallet-connect/hooks/useWalletConnect.ts | 264 ++++-------------- src/components/wallet-connect/index.tsx | 9 - src/safe-wallet-provider/provider.ts | 3 + .../useSafeWalletProvider.tsx | 17 +- 5 files changed, 337 insertions(+), 215 deletions(-) create mode 100644 src/components/wallet-connect/hooks/WalletConnect.ts diff --git a/src/components/wallet-connect/hooks/WalletConnect.ts b/src/components/wallet-connect/hooks/WalletConnect.ts new file mode 100644 index 0000000000..4108b78a08 --- /dev/null +++ b/src/components/wallet-connect/hooks/WalletConnect.ts @@ -0,0 +1,259 @@ +import { IS_PRODUCTION, WALLETCONNECT_V2_PROJECT_ID } from '@/config/constants' +import { Core } from '@walletconnect/core' +import { Web3Wallet, type Web3WalletTypes } from '@walletconnect/web3wallet' +import { type JsonRpcResponse } from '@walletconnect/jsonrpc-utils' + +import type Web3WalletType from '@walletconnect/web3wallet' +import { type SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type SessionTypes } from 'walletconnect-v2-types' + +const logger = IS_PRODUCTION ? undefined : 'debug' + +const USER_DISCONNECTED_CODE = 6000 + +const EVMBasedNamespaces: string = 'eip155' + +// see full list here: https://github.com/safe-global/safe-apps-sdk/blob/main/packages/safe-apps-provider/src/provider.ts#L35 +export const compatibleSafeMethods: string[] = [ + 'eth_accounts', + 'net_version', + 'eth_chainId', + 'personal_sign', + 'eth_sign', + 'eth_signTypedData', + 'eth_signTypedData_v4', + 'eth_sendTransaction', + 'eth_blockNumber', + 'eth_getBalance', + 'eth_getCode', + 'eth_getTransactionCount', + 'eth_getStorageAt', + 'eth_getBlockByNumber', + 'eth_getBlockByHash', + 'eth_getTransactionByHash', + 'eth_getTransactionReceipt', + 'eth_estimateGas', + 'eth_call', + 'eth_getLogs', + 'eth_gasPrice', + 'wallet_getPermissions', + 'wallet_requestPermissions', + 'safe_setSettings', +] + +// MOVE TO CONSTANTS +export const SAFE_WALLET_METADATA = { + name: 'Safe Wallet', + description: 'The most trusted platform to manage digital assets on Ethereum', + url: 'https://app.safe.global', + icons: ['https://app.safe.global/favicons/mstile-150x150.png', 'https://app.safe.global/favicons/logo_120x120.png'], +} + +export class WalletConnect { + #web3Wallet: Web3WalletType | undefined + #safe: SafeInfo + #currentSession: SessionTypes.Struct | undefined + + constructor(safe: SafeInfo) { + this.#safe = safe + this.initializeWalletConnect() + } + + private initializeWalletConnect = async () => { + await this.initializeWalletConnectV2Client() + } + + private initializeWalletConnectV2Client = async () => { + const core = new Core({ + projectId: WALLETCONNECT_V2_PROJECT_ID, + logger, + }) + + const web3wallet = await Web3Wallet.init({ + core, + metadata: SAFE_WALLET_METADATA, + }) + + this.#web3Wallet = web3wallet + } + + isConnected = () => !!this.#currentSession + + getMetadata = () => this.#currentSession?.peer.metadata + + getSessionTopic = () => this.#currentSession?.topic + + restoreExistingConnection = () => { + console.log('Trying to restore for safe', this.#safe) + if (!this.#web3Wallet) { + return + } + // we try to find a compatible active session + const activeSessions = this.#web3Wallet.getActiveSessions() + console.log('Active Sessions', activeSessions) + + const compatibleSession = Object.keys(activeSessions) + .map((topic) => activeSessions[topic]) + .find((session) => + session.namespaces[EVMBasedNamespaces].accounts[0].includes( + `${EVMBasedNamespaces}:${this.#safe.chainId}:${this.#safe.address.value}`, + ), + ) + + if (compatibleSession) { + this.#currentSession = compatibleSession + } + } + + connect = async (uri: string) => { + const isValidWalletConnectUri = uri && uri.startsWith('wc') + + if (isValidWalletConnectUri && this.#web3Wallet) { + await this.#web3Wallet.core.pairing.pair({ uri }) + } + } + + disconnect = async () => { + if (!this.#web3Wallet || !this.#currentSession) { + throw Error('Cannot disconnect if no session is active') + } + await this.#web3Wallet.disconnectSession({ + topic: this.#currentSession.topic, + reason: { + code: USER_DISCONNECTED_CODE, + message: 'User disconnected. Safe Wallet Session ended by the user', + }, + }) + this.#currentSession = undefined + } + + resetSession = () => { + this.#currentSession = undefined + } + + onSessionProposal = (handler: (proposal: Web3WalletTypes.SessionProposal) => void) => { + // events + this.#web3Wallet?.on('session_proposal', handler) + } + + onSessionDelete = (handler: () => void) => { + this.#web3Wallet?.on('session_delete', handler) + } + + onSessionRequest = (handler: (event: Web3WalletTypes.SessionRequest) => void) => { + console.log('Registering session request handler') + this.#web3Wallet?.on('session_request', handler) + } + + approveSessionProposal = async ( + sessionProposal: Web3WalletTypes.SessionProposal, + onMismatchingNamespaces: () => void, + ) => { + if (!this.#web3Wallet) { + throw new Error('Web3Wallet needs to be initialized first') + } + const { id, params } = sessionProposal + const { requiredNamespaces } = params + const requiredNamespace = requiredNamespaces[EVMBasedNamespaces] + + const safeChain = `${EVMBasedNamespaces}:${this.#safe.chainId}` + const safeEvents = requiredNamespace?.events || [] // we accept all events like chainChanged & accountsChanged (even if they are not compatible with the Safe) + + const requiredChains = [...(requiredNamespace.chains ?? [])] + // If the user accepts we always return all required namespaces and add the safe chain to it + const safeAccount = `${EVMBasedNamespaces}:${this.#safe.chainId}:${this.#safe.address.value}` + + // We first fake that our Safe is available on all required networks + const safeOnRequiredChains = requiredChains.map( + (requiredChain) => `${requiredChain ?? safeChain}:${this.#safe.address.value}`, + ) + + let wcSession: SessionTypes.Struct + try { + wcSession = await this.#web3Wallet.approveSession({ + id, + namespaces: { + eip155: { + accounts: [safeAccount], // only the Safe account + chains: [safeChain], // only the Safe chain + methods: compatibleSafeMethods, // only the Safe methods + events: safeEvents, + }, + }, + }) + } catch (error) { + wcSession = await this.#web3Wallet.approveSession({ + id, + namespaces: { + eip155: { + accounts: safeOnRequiredChains.includes(safeAccount) + ? safeOnRequiredChains + : [...safeOnRequiredChains, safeAccount], // Add all required chains on top + chains: requiredChains, // return the required Safes + methods: compatibleSafeMethods, // only the Safe methods + events: safeEvents, + }, + }, + }) + } + + this.#currentSession = wcSession + + // Then we update the session and reduce the Safe to the requested network only + if (!safeOnRequiredChains.includes(safeAccount) || safeOnRequiredChains.length > 1) { + if (!requiredChains.includes(safeChain)) { + requiredChains.push(safeChain) + } + + // Emit accountsChanged and chainChanged event + try { + await this.#web3Wallet.updateSession({ + topic: wcSession.topic, + namespaces: { + eip155: { + accounts: [safeAccount], + chains: requiredChains, + methods: compatibleSafeMethods, + events: safeEvents, + }, + }, + }) + } catch (error) { + onMismatchingNamespaces() + } + } + } + + sendSessionResponse = async (params: { topic: string; response: JsonRpcResponse }) => { + if (!this.#web3Wallet) { + throw new Error('Web3Wallet needs to be initialized first') + } + + await this.#web3Wallet.respondSessionRequest(params) + } + + updateSafeInfo = async (safe: SafeInfo) => { + this.#safe = safe + + if (!this.#currentSession || !this.#web3Wallet) { + return + } + + // We have to update the active session + const safeAccount = `${EVMBasedNamespaces}:${safe.chainId}:${safe.address.value}` + const safeChain = `${EVMBasedNamespaces}:${safe.chainId}` + const currentNamespace = this.#currentSession.namespaces[EVMBasedNamespaces] + const chainIsSet = currentNamespace.chains?.includes(safeChain) + + await this.#web3Wallet.updateSession({ + topic: this.#currentSession.topic, + namespaces: { + eip155: { + ...currentNamespace, + accounts: [safeAccount], + chains: chainIsSet ? currentNamespace.chains : [...(currentNamespace.chains ?? []), safeChain], + }, + }, + }) + } +} diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index e46605c7da..91915eb70f 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -1,49 +1,17 @@ import { useState, useCallback, useEffect } from 'react' -import type { SignClientTypes, SessionTypes } from 'walletconnect-v2-types' -import { Core } from '@walletconnect/core' -import type Web3WalletType from '@walletconnect/web3wallet' -import { Web3Wallet, type Web3WalletTypes } from '@walletconnect/web3wallet' -import { IS_PRODUCTION, WALLETCONNECT_V2_PROJECT_ID } from '@/config/constants' +import type { SignClientTypes } from 'walletconnect-v2-types' +import { type Web3WalletTypes } from '@walletconnect/web3wallet' import { useCurrentChain } from '@/hooks/useChains' import useSafeInfo from '@/hooks/useSafeInfo' +import useSafeWalletProvider from '@/safe-wallet-provider/useSafeWalletProvider' +import { WalletConnect } from './WalletConnect' const EVMBasedNamespaces: string = 'eip155' -// see full list here: https://github.com/safe-global/safe-apps-sdk/blob/main/packages/safe-apps-provider/src/provider.ts#L35 -export const compatibleSafeMethods: string[] = [ - 'eth_accounts', - 'net_version', - 'eth_chainId', - 'personal_sign', - 'eth_sign', - 'eth_signTypedData', - 'eth_signTypedData_v4', - 'eth_sendTransaction', - 'eth_blockNumber', - 'eth_getBalance', - 'eth_getCode', - 'eth_getTransactionCount', - 'eth_getStorageAt', - 'eth_getBlockByNumber', - 'eth_getBlockByHash', - 'eth_getTransactionByHash', - 'eth_getTransactionReceipt', - 'eth_estimateGas', - 'eth_call', - 'eth_getLogs', - 'eth_gasPrice', - 'wallet_getPermissions', - 'wallet_requestPermissions', - 'safe_setSettings', -] - // see https://docs.walletconnect.com/2.0/specs/sign/error-codes const UNSUPPORTED_CHAIN_ERROR_CODE = 5100 const INVALID_METHOD_ERROR_CODE = 1001 const USER_REJECTED_REQUEST_CODE = 4001 -const USER_DISCONNECTED_CODE = 6000 - -const logger = IS_PRODUCTION ? undefined : 'debug' export const errorLabel = 'We were unable to create a connection due to compatibility issues with the latest WalletConnect v2 upgrade. We are actively working with the WalletConnect team and the dApps to get these issues resolved. Use Safe Apps instead wherever possible.' @@ -56,21 +24,12 @@ export type useWalletConnectType = { wcConnect: wcConnectType wcDisconnect: wcDisconnectType wcApproveSession: () => Promise - isWallectConnectInitialized: boolean error: string | undefined sessionProposal: Web3WalletTypes.SessionProposal | undefined wcState: WC_CONNECT_STATE acceptInvalidSession: () => void } -// MOVE TO CONSTANTS -export const SAFE_WALLET_METADATA = { - name: 'Safe Wallet', - description: 'The most trusted platform to manage digital assets on Ethereum', - url: 'https://app.safe.global', - icons: ['https://app.safe.global/favicons/mstile-150x150.png', 'https://app.safe.global/favicons/logo_120x120.png'], -} - export enum WC_CONNECT_STATE { NOT_CONNECTED, PAIRING_SESSION, @@ -82,49 +41,44 @@ export enum WC_CONNECT_STATE { } const useWalletConnect = (): useWalletConnectType => { - const [web3wallet, setWeb3wallet] = useState() const [sessionProposal, setSessionProposal] = useState() const [wcState, setWcState] = useState(WC_CONNECT_STATE.NOT_CONNECTED) - const [wcSession, setWcSession] = useState() - const [isWallectConnectInitialized, setIsWallectConnectInitialized] = useState(false) const [error, setError] = useState() const chainInfo = useCurrentChain() const { safe } = useSafeInfo() - // Initializing v2, see https://docs.walletconnect.com/2.0/javascript/web3wallet/wallet-usage + const [walletConnect, setWalletConnect] = useState() + + const safeWalletProvider = useSafeWalletProvider() + + const currentSessionTopic = walletConnect?.getSessionTopic() + + // Initialize useEffect(() => { - const initializeWalletConnectV2Client = async () => { - const core = new Core({ - projectId: WALLETCONNECT_V2_PROJECT_ID, - logger, - }) - - const web3wallet = await Web3Wallet.init({ - core, - metadata: SAFE_WALLET_METADATA, - }) - - setWeb3wallet(web3wallet) + if (!walletConnect) { + setWalletConnect(new WalletConnect(safe)) } + }, [safe, walletConnect]) - try { - initializeWalletConnectV2Client() - } catch (error) { - console.log('Error on walletconnect version 2 initialization: ', error) - setIsWallectConnectInitialized(true) + // If connected Safe / chain changes we have to update the session + useEffect(() => { + if (!walletConnect) { + return } - }, []) - // session_request needs to be a separate Effect because a valid wcSession should be present + walletConnect.updateSafeInfo(safe) + }, [safe.address.value, safe.chainId, safe, walletConnect]) + useEffect(() => { - if (!isWallectConnectInitialized || !web3wallet || !wcSession) { + console.log('Trying to register') + if (!walletConnect || !safeWalletProvider) { return } - web3wallet.on('session_request', async (event) => { + console.log('Registering session request handler', currentSessionTopic) + walletConnect.onSessionRequest(async (event) => { const { topic, id } = event const { request, chainId: transactionChainId } = event.params - const { method, params } = request const isSafeChainId = transactionChainId === `${EVMBasedNamespaces}:${safe.chainId}` @@ -132,7 +86,7 @@ const useWalletConnect = (): useWalletConnectType => { if (!isSafeChainId) { const errorMessage = `Transaction rejected: the connected Dapp is not set to the correct chain. Make sure the Dapp only uses ${chainInfo?.chainName} to interact with this Safe.` setError(errorMessage) - await web3wallet.respondSessionRequest({ + await walletConnect.sendSessionResponse({ topic, response: rejectResponse(id, UNSUPPORTED_CHAIN_ERROR_CODE, errorMessage), }) @@ -141,9 +95,8 @@ const useWalletConnect = (): useWalletConnectType => { try { setError(undefined) - // Handle request - /* const result = await web3Provider.send(method, params) - await web3wallet.respondSessionRequest({ + const result = await safeWalletProvider.request(request) + await walletConnect.sendSessionResponse({ topic, response: { id, @@ -151,150 +104,63 @@ const useWalletConnect = (): useWalletConnectType => { result, }, }) - */ + // TODO TRACKING // trackEvent(TRANSACTION_CONFIRMED_ACTION, WALLET_CONNECT_VERSION_2, wcSession.peer.metadata) } catch (error: any) { setError(error?.message) const isUserRejection = error?.message?.includes?.('Transaction was rejected') const code = isUserRejection ? USER_REJECTED_REQUEST_CODE : INVALID_METHOD_ERROR_CODE - await web3wallet.respondSessionRequest({ + await walletConnect.sendSessionResponse({ topic, response: rejectResponse(id, code, error.message), }) } }) - }, [chainInfo, wcSession, isWallectConnectInitialized, web3wallet, safe]) + }, [chainInfo?.chainName, safe.chainId, safeWalletProvider, walletConnect, currentSessionTopic]) // we set here the events & restore an active previous session useEffect(() => { - if (!isWallectConnectInitialized && web3wallet) { - // we try to find a compatible active session - const activeSessions = web3wallet.getActiveSessions() - const compatibleSession = Object.keys(activeSessions) - .map((topic) => activeSessions[topic]) - .find( - (session) => - session.namespaces[EVMBasedNamespaces].accounts[0] === - `${EVMBasedNamespaces}:${safe.chainId}:${safe.address.value}`, // Safe Account - ) - - if (compatibleSession) { - setWcSession(compatibleSession) - setWcState(WC_CONNECT_STATE.CONNECTED) - } + walletConnect?.restoreExistingConnection() - // events - web3wallet.on('session_proposal', async (proposal) => { - setSessionProposal(proposal) - setWcState(WC_CONNECT_STATE.PENDING_SESSION_REQUEST) - }) + if (walletConnect?.isConnected()) { + setWcState(WC_CONNECT_STATE.CONNECTED) + } - web3wallet.on('session_delete', async () => { - setWcState(WC_CONNECT_STATE.NOT_CONNECTED) - setWcSession(undefined) - setError(undefined) - }) + // events + walletConnect?.onSessionProposal(async (proposal) => { + setSessionProposal(proposal) + setWcState(WC_CONNECT_STATE.PENDING_SESSION_REQUEST) + }) - setIsWallectConnectInitialized(true) - } - }, [safe, web3wallet, isWallectConnectInitialized, chainInfo]) + walletConnect?.onSessionDelete(async () => { + walletConnect.resetSession() + setWcState(WC_CONNECT_STATE.NOT_CONNECTED) + setError(undefined) + }) + }, [safe, chainInfo, walletConnect]) const wcConnect = useCallback( async (uri: string) => { - const isValidWalletConnectUri = uri && uri.startsWith('wc') - - if (isValidWalletConnectUri && web3wallet) { - setWcState(WC_CONNECT_STATE.PAIRING_SESSION) - await web3wallet.core.pairing.pair({ uri }) - } + await walletConnect?.connect(uri) + setWcState(WC_CONNECT_STATE.PAIRING_SESSION) }, - [web3wallet], + [walletConnect], ) const wcApproveSession = useCallback(async () => { - if (!sessionProposal || !web3wallet) { + if (!sessionProposal || !walletConnect) { throw new Error('Cannot approve session without pending session proposal') } - console.log('Approving session', sessionProposal) - const { id, params } = sessionProposal - const { requiredNamespaces } = params - const requiredNamespace = requiredNamespaces[EVMBasedNamespaces] - - const safeChain = `${EVMBasedNamespaces}:${safe.chainId}` - const safeEvents = requiredNamespace?.events || [] // we accept all events like chainChanged & accountsChanged (even if they are not compatible with the Safe) - - const requiredChains = [...(requiredNamespace.chains ?? [])] - // If the user accepts we always return all required namespaces and add the safe chain to it - const safeAccount = `${EVMBasedNamespaces}:${safe.chainId}:${safe.address.value}` - - // We first fake that our Safe is available on all required networks - const safeOnRequiredChains = requiredChains.map( - (requiredChain) => `${requiredChain ?? safeChain}:${safe.address.value}`, - ) - - let wcSession: SessionTypes.Struct - try { - wcSession = await web3wallet.approveSession({ - id, - namespaces: { - eip155: { - accounts: [safeAccount], // only the Safe account - chains: [safeChain], // only the Safe chain - methods: compatibleSafeMethods, // only the Safe methods - events: safeEvents, - }, - }, - }) - } catch (error) { - wcSession = await web3wallet.approveSession({ - id, - namespaces: { - eip155: { - accounts: safeOnRequiredChains.includes(safeAccount) - ? safeOnRequiredChains - : [...safeOnRequiredChains, safeAccount], // Add all required chains on top - chains: requiredChains, // return the required Safes - methods: compatibleSafeMethods, // only the Safe methods - events: safeEvents, - }, - }, - }) - } - - // Then we update the session and reduce the Safe to the requested network only - if (!safeOnRequiredChains.includes(safeAccount) || safeOnRequiredChains.length > 1) { - if (!requiredChains.includes(safeChain)) { - requiredChains.push(safeChain) - } - - // Emit accountsChanged and chainChanged event - try { - await web3wallet.updateSession({ - topic: wcSession.topic, - namespaces: { - eip155: { - accounts: [safeAccount], - chains: requiredChains, - methods: compatibleSafeMethods, - events: safeEvents, - }, - }, - }) - } catch (error) { - setWcState(WC_CONNECT_STATE.APPROVE_INVALID_SESSION) - setWcSession(wcSession) - return - } - } + await walletConnect.approveSessionProposal(sessionProposal, () => { + setWcState(WC_CONNECT_STATE.APPROVE_INVALID_SESSION) + }) - // - setWcSession(wcSession) setSessionProposal(undefined) setError(undefined) setWcState(WC_CONNECT_STATE.CONNECTED) - }, [safe.address, safe.chainId, sessionProposal, web3wallet]) + }, [sessionProposal, walletConnect]) const acceptInvalidSession = useCallback(() => { setWcState(WC_CONNECT_STATE.CONNECTED) @@ -303,29 +169,21 @@ const useWalletConnect = (): useWalletConnectType => { }, []) const wcDisconnect = useCallback(async () => { - if (wcSession && web3wallet) { - await web3wallet.disconnectSession({ - topic: wcSession.topic, - reason: { - code: USER_DISCONNECTED_CODE, - message: 'User disconnected. Safe Wallet Session ended by the user', - }, - }) - setWcState(WC_CONNECT_STATE.NOT_CONNECTED) - - setWcSession(undefined) - setError(undefined) + if (!walletConnect) { + throw new Error('WalletConnect is not initialized') } - }, [web3wallet, wcSession]) + await walletConnect?.disconnect() + setWcState(WC_CONNECT_STATE.NOT_CONNECTED) + setError(undefined) + }, [walletConnect]) - const wcClientData = wcSession?.peer.metadata + const wcClientData = walletConnect?.getMetadata() return { wcConnect, wcClientData, wcDisconnect, wcApproveSession, - isWallectConnectInitialized, error, wcState, sessionProposal, diff --git a/src/components/wallet-connect/index.tsx b/src/components/wallet-connect/index.tsx index 0ea160a178..05a696b8e5 100644 --- a/src/components/wallet-connect/index.tsx +++ b/src/components/wallet-connect/index.tsx @@ -39,25 +39,18 @@ const extractInformationFromProposal = (sessionProposal: Web3WalletTypes.Session export const ConnectWC = () => { const [openModal, setOpenModal] = useState(false) const [wcConnectUrl, setWcConnectUrl] = useState('') - const [isConnecting, setIsConnecting] = useState(false) const { wcConnect, wcDisconnect, wcClientData, wcApproveSession, acceptInvalidSession, wcState, sessionProposal } = useWalletConnect() const anchorElem = useRef(null) const chains = useChains() const { safe } = useSafeInfo() - const isConnected = !!wcClientData - const proposalInfo = extractInformationFromProposal(sessionProposal) const unsupportedChains = proposalInfo?.requiredNamespaces.chains?.find( (chain) => safe.chainId !== chain.slice(EVMBasedNamespaces.length + 1), ) - console.log('Unsupported chains', unsupportedChains) - - console.log('WcClientData', wcClientData) - const handleWidgetIconClick = () => { setOpenModal((prev) => !prev) } @@ -65,7 +58,6 @@ export const ConnectWC = () => { const onConnect = useCallback( async (uri: string) => { await wcConnect(uri) - setIsConnecting(false) }, [wcConnect], ) @@ -79,7 +71,6 @@ export const ConnectWC = () => { (event: React.ClipboardEvent) => { const connectWithUri = (data: string) => { if (data.startsWith('wc')) { - setIsConnecting(true) onConnect(data) } } diff --git a/src/safe-wallet-provider/provider.ts b/src/safe-wallet-provider/provider.ts index bcc16e6ac1..8efd7b3d77 100644 --- a/src/safe-wallet-provider/provider.ts +++ b/src/safe-wallet-provider/provider.ts @@ -29,6 +29,8 @@ export class SafeWalletProvider { async request(request: RpcRequest): Promise { const { method, params = [] } = request + console.log('SafeWalletProvider request', request) + switch (method) { case 'eth_accounts': return [this.safe.safeAddress] @@ -78,6 +80,7 @@ export class SafeWalletProvider { } case 'eth_sendTransaction': + console.log(params) const tx = { value: '0', data: '0x', diff --git a/src/safe-wallet-provider/useSafeWalletProvider.tsx b/src/safe-wallet-provider/useSafeWalletProvider.tsx index 87c6369ded..b42cc44879 100644 --- a/src/safe-wallet-provider/useSafeWalletProvider.tsx +++ b/src/safe-wallet-provider/useSafeWalletProvider.tsx @@ -6,9 +6,11 @@ import SignMessageFlow from '@/components/tx-flow/flows/SignMessage' import { safeMsgSubscribe, SafeMsgEvent } from '@/services/safe-messages/safeMsgEvents' import SafeAppsTxFlow from '@/components/tx-flow/flows/SafeAppsTx' import { TxEvent, txSubscribe } from '@/services/tx/txEvents' -import type { BaseTransaction, EIP712TypedData } from '@safe-global/safe-apps-sdk' +import type { EIP712TypedData } from '@safe-global/safe-apps-sdk' import { useWeb3ReadOnly } from '@/hooks/wallets/web3' import { getTransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' +import { getAddress } from 'ethers/lib/utils' +import { BigNumber } from 'ethers' const useSafeWalletProvider = (): SafeWalletProvider | undefined => { const { safe, safeAddress } = useSafeInfo() @@ -40,15 +42,24 @@ const useSafeWalletProvider = (): SafeWalletProvider | undefined => { return this.signMessage(typedData as EIP712TypedData) }, - async send(params: { txs: unknown[]; params: { safeTxGas: number } }): Promise<{ safeTxHash: string }> { + async send(params: { txs: any[]; params: { safeTxGas: number } }): Promise<{ safeTxHash: string }> { const id = Math.random().toString(36).slice(2) + console.log('Opening tx flow', params) + + const transactions = params.txs.map(({ to, value, data }) => { + return { + to: getAddress(to), + value: BigNumber.from(value).toString(), + data, + } + }) setTxFlow( , From 7d2c42ac40d61be3dbe7c24d64b394b89c19ab32 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Fri, 15 Sep 2023 14:31:17 +0200 Subject: [PATCH 09/17] Add connect page --- src/pages/connect.tsx | 31 ++++++++++++++++ src/safe-wallet-provider/provider.ts | 21 +++++++++-- .../useSafeWalletConnect.ts | 35 +++++++++++++++++++ .../useSafeWalletProvider.tsx | 3 +- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 src/pages/connect.tsx create mode 100644 src/safe-wallet-provider/useSafeWalletConnect.ts diff --git a/src/pages/connect.tsx b/src/pages/connect.tsx new file mode 100644 index 0000000000..caa3a96b19 --- /dev/null +++ b/src/pages/connect.tsx @@ -0,0 +1,31 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import { useRouter } from 'next/router' +import useLastSafe from '@/hooks/useLastSafe' +import useSafeWalletConnect from '@/safe-wallet-provider/useSafeWalletConnect' + +const Connect: NextPage = () => { + const router = useRouter() + const { safe } = router.query + const lastSafe = useLastSafe() + + if (!safe && lastSafe) { + router.replace(`/connect?safe=${lastSafe}`) + } + + useSafeWalletConnect() + + return ( + <> + + {'Safe{Wallet} – Connect'} + + +
+

Connect

+
+ + ) +} + +export default Connect diff --git a/src/safe-wallet-provider/provider.ts b/src/safe-wallet-provider/provider.ts index bcc16e6ac1..0e77616fd9 100644 --- a/src/safe-wallet-provider/provider.ts +++ b/src/safe-wallet-provider/provider.ts @@ -8,10 +8,11 @@ type WalletSDK = { signTypedMessage: (typedData: unknown) => Promise<{ signature?: string }> send: (params: { txs: unknown[]; params: { safeTxGas: number } }) => Promise<{ safeTxHash: string }> getBySafeTxHash: (safeTxHash: string) => Promise<{ txHash?: string }> - proxy: (method: string, params: unknown[]) => Promise + proxy: (method: string, params: unknown[]) => Promise<{ result: unknown }> } interface RpcRequest { + id: number method: string params?: unknown[] } @@ -26,10 +27,13 @@ export class SafeWalletProvider { this.sdk = sdk } - async request(request: RpcRequest): Promise { + private async makeRequest(request: RpcRequest): Promise { const { method, params = [] } = request switch (method) { + case 'wallet_switchEthereumChain': + return true + case 'eth_accounts': return [this.safe.safeAddress] @@ -137,4 +141,17 @@ export class SafeWalletProvider { return await this.sdk.proxy(method, params) } } + + async request(request: RpcRequest): Promise<{ + jsonrpc: string + id: number + result?: unknown + }> { + const result = await this.makeRequest(request) + return { + jsonrpc: '2.0', + id: request.id, + result, + } + } } diff --git a/src/safe-wallet-provider/useSafeWalletConnect.ts b/src/safe-wallet-provider/useSafeWalletConnect.ts new file mode 100644 index 0000000000..0bbe5946cc --- /dev/null +++ b/src/safe-wallet-provider/useSafeWalletConnect.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react' +import useSafeWalletProvider from './useSafeWalletProvider' + +const useSafeWalletConnect = () => { + const safeWalletProvider = useSafeWalletProvider() + + useEffect(() => { + if (!safeWalletProvider) return + + const handler = async (e: MessageEvent) => { + if (e.origin === location.origin) return + + if (e.data.safeRpcRequest) { + const response = await safeWalletProvider.request(e.data.safeRpcRequest) + + window.opener?.postMessage( + { + safeRpcResponse: response, + }, + e.origin, + ) + } + } + + window.addEventListener('message', handler) + + window.opener?.postMessage('safeWalletLoaded', '*') + + return () => { + window.removeEventListener('message', handler) + } + }, [safeWalletProvider]) +} + +export default useSafeWalletConnect diff --git a/src/safe-wallet-provider/useSafeWalletProvider.tsx b/src/safe-wallet-provider/useSafeWalletProvider.tsx index 87c6369ded..1449e35c9d 100644 --- a/src/safe-wallet-provider/useSafeWalletProvider.tsx +++ b/src/safe-wallet-provider/useSafeWalletProvider.tsx @@ -69,7 +69,8 @@ const useSafeWalletProvider = (): SafeWalletProvider | undefined => { }, async proxy(method: string, params: unknown[]) { - return web3ReadOnly?.send(method, params) + const data = await web3ReadOnly?.send(method, params) + return data.result }, } }, [safeAddress, chainId, setTxFlow, web3ReadOnly]) From 73048270cdbf8491d37604b0f4dc9f39e2cd4a07 Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 18 Sep 2023 09:33:07 +0200 Subject: [PATCH 10/17] fix: wording and order of accounts --- src/components/wallet-connect/hooks/WalletConnect.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/wallet-connect/hooks/WalletConnect.ts b/src/components/wallet-connect/hooks/WalletConnect.ts index 4108b78a08..aa5e22f1b4 100644 --- a/src/components/wallet-connect/hooks/WalletConnect.ts +++ b/src/components/wallet-connect/hooks/WalletConnect.ts @@ -163,7 +163,7 @@ export class WalletConnect { // If the user accepts we always return all required namespaces and add the safe chain to it const safeAccount = `${EVMBasedNamespaces}:${this.#safe.chainId}:${this.#safe.address.value}` - // We first fake that our Safe is available on all required networks + // We first pretend that our Safe is available on all required networks const safeOnRequiredChains = requiredChains.map( (requiredChain) => `${requiredChain ?? safeChain}:${this.#safe.address.value}`, ) @@ -188,7 +188,7 @@ export class WalletConnect { eip155: { accounts: safeOnRequiredChains.includes(safeAccount) ? safeOnRequiredChains - : [...safeOnRequiredChains, safeAccount], // Add all required chains on top + : [safeAccount, ...safeOnRequiredChains], // Add all required chains on top chains: requiredChains, // return the required Safes methods: compatibleSafeMethods, // only the Safe methods events: safeEvents, From 195e7a11638a8320e365aa6120e1760f8849d055 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 19 Sep 2023 12:21:28 +0200 Subject: [PATCH 11/17] Use WC_PROJECT_ID --- src/components/wallet-connect/hooks/WalletConnect.ts | 4 ++-- src/components/wallet-connect/index.tsx | 2 ++ src/config/constants.ts | 3 --- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/wallet-connect/hooks/WalletConnect.ts b/src/components/wallet-connect/hooks/WalletConnect.ts index aa5e22f1b4..b04c45d2f6 100644 --- a/src/components/wallet-connect/hooks/WalletConnect.ts +++ b/src/components/wallet-connect/hooks/WalletConnect.ts @@ -1,4 +1,4 @@ -import { IS_PRODUCTION, WALLETCONNECT_V2_PROJECT_ID } from '@/config/constants' +import { IS_PRODUCTION, WC_PROJECT_ID } from '@/config/constants' import { Core } from '@walletconnect/core' import { Web3Wallet, type Web3WalletTypes } from '@walletconnect/web3wallet' import { type JsonRpcResponse } from '@walletconnect/jsonrpc-utils' @@ -65,7 +65,7 @@ export class WalletConnect { private initializeWalletConnectV2Client = async () => { const core = new Core({ - projectId: WALLETCONNECT_V2_PROJECT_ID, + projectId: WC_PROJECT_ID, logger, }) diff --git a/src/components/wallet-connect/index.tsx b/src/components/wallet-connect/index.tsx index 05a696b8e5..8ad3b45c43 100644 --- a/src/components/wallet-connect/index.tsx +++ b/src/components/wallet-connect/index.tsx @@ -221,6 +221,8 @@ export const ConnectWC = () => { label="Wallet Connect URI" onPaste={onPaste} onChange={onChangeWcUrl} + fullWidth + sx={{ mt: 2 }} >
)} diff --git a/src/config/constants.ts b/src/config/constants.ts index d20382e40f..577265e35f 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -97,6 +97,3 @@ export const IS_OFFICIAL_HOST = process.env.NEXT_PUBLIC_IS_OFFICIAL_HOST || fals export const REDEFINE_SIMULATION_URL = 'https://dashboard.redefine.net/reports/' export const REDEFINE_API = process.env.NEXT_PUBLIC_REDEFINE_API export const REDEFINE_ARTICLE = 'https://safe.mirror.xyz/rInLWZwD_sf7enjoFerj6FIzCYmVMGrrV8Nhg4THdwI' - -// Wallet Connect v2 -export const WALLETCONNECT_V2_PROJECT_ID = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID From 1e84f48b83e1a54acf4a3652b2d2770dc6ce3a58 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 19 Sep 2023 12:51:33 +0200 Subject: [PATCH 12/17] Unsubscribe in use effect --- src/components/wallet-connect/hooks/WalletConnect.ts | 3 +++ .../wallet-connect/hooks/useWalletConnect.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/wallet-connect/hooks/WalletConnect.ts b/src/components/wallet-connect/hooks/WalletConnect.ts index b04c45d2f6..e42e29e97e 100644 --- a/src/components/wallet-connect/hooks/WalletConnect.ts +++ b/src/components/wallet-connect/hooks/WalletConnect.ts @@ -134,15 +134,18 @@ export class WalletConnect { onSessionProposal = (handler: (proposal: Web3WalletTypes.SessionProposal) => void) => { // events this.#web3Wallet?.on('session_proposal', handler) + return () => this.#web3Wallet?.off('session_proposal', handler) } onSessionDelete = (handler: () => void) => { this.#web3Wallet?.on('session_delete', handler) + return () => this.#web3Wallet?.off('session_delete', handler) } onSessionRequest = (handler: (event: Web3WalletTypes.SessionRequest) => void) => { console.log('Registering session request handler') this.#web3Wallet?.on('session_request', handler) + return () => this.#web3Wallet?.off('session_request', handler) } approveSessionProposal = async ( diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index 91915eb70f..7491ad995e 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -76,7 +76,7 @@ const useWalletConnect = (): useWalletConnectType => { return } console.log('Registering session request handler', currentSessionTopic) - walletConnect.onSessionRequest(async (event) => { + return walletConnect.onSessionRequest(async (event) => { const { topic, id } = event const { request, chainId: transactionChainId } = event.params @@ -126,19 +126,23 @@ const useWalletConnect = (): useWalletConnectType => { if (walletConnect?.isConnected()) { setWcState(WC_CONNECT_STATE.CONNECTED) } + }, [walletConnect]) + useEffect(() => { // events - walletConnect?.onSessionProposal(async (proposal) => { + return walletConnect?.onSessionProposal(async (proposal) => { setSessionProposal(proposal) setWcState(WC_CONNECT_STATE.PENDING_SESSION_REQUEST) }) + }, [safe, chainInfo, walletConnect]) - walletConnect?.onSessionDelete(async () => { + useEffect(() => { + return walletConnect?.onSessionDelete(async () => { walletConnect.resetSession() setWcState(WC_CONNECT_STATE.NOT_CONNECTED) setError(undefined) }) - }, [safe, chainInfo, walletConnect]) + }, [walletConnect]) const wcConnect = useCallback( async (uri: string) => { From ee32555f0e9b15343a69e83595352157ff78b793 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 19 Sep 2023 14:47:04 +0200 Subject: [PATCH 13/17] Class syntax --- .../wallet-connect/hooks/WalletConnect.ts | 121 +++++++++--------- .../wallet-connect/hooks/useWalletConnect.ts | 7 +- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/src/components/wallet-connect/hooks/WalletConnect.ts b/src/components/wallet-connect/hooks/WalletConnect.ts index e42e29e97e..ab2dd6e07c 100644 --- a/src/components/wallet-connect/hooks/WalletConnect.ts +++ b/src/components/wallet-connect/hooks/WalletConnect.ts @@ -50,20 +50,16 @@ export const SAFE_WALLET_METADATA = { } export class WalletConnect { - #web3Wallet: Web3WalletType | undefined - #safe: SafeInfo - #currentSession: SessionTypes.Struct | undefined + private web3Wallet: Web3WalletType | undefined + private safe: SafeInfo + private currentSession: SessionTypes.Struct | undefined constructor(safe: SafeInfo) { - this.#safe = safe + this.safe = safe this.initializeWalletConnect() } - private initializeWalletConnect = async () => { - await this.initializeWalletConnectV2Client() - } - - private initializeWalletConnectV2Client = async () => { + private async initializeWalletConnect() { const core = new Core({ projectId: WC_PROJECT_ID, logger, @@ -74,106 +70,113 @@ export class WalletConnect { metadata: SAFE_WALLET_METADATA, }) - this.#web3Wallet = web3wallet + this.web3Wallet = web3wallet } - isConnected = () => !!this.#currentSession + isConnected() { + return !!this.currentSession + } - getMetadata = () => this.#currentSession?.peer.metadata + getMetadata() { + return this.currentSession?.peer.metadata + } - getSessionTopic = () => this.#currentSession?.topic + getSessionTopic() { + return this.currentSession?.topic + } - restoreExistingConnection = () => { - console.log('Trying to restore for safe', this.#safe) - if (!this.#web3Wallet) { + restoreExistingConnection() { + console.log('WC trying to restore for safe', this.safe) + + if (!this.web3Wallet) { return } // we try to find a compatible active session - const activeSessions = this.#web3Wallet.getActiveSessions() + const activeSessions = this.web3Wallet.getActiveSessions() console.log('Active Sessions', activeSessions) const compatibleSession = Object.keys(activeSessions) .map((topic) => activeSessions[topic]) .find((session) => session.namespaces[EVMBasedNamespaces].accounts[0].includes( - `${EVMBasedNamespaces}:${this.#safe.chainId}:${this.#safe.address.value}`, + `${EVMBasedNamespaces}:${this.safe.chainId}:${this.safe.address.value}`, ), ) if (compatibleSession) { - this.#currentSession = compatibleSession + this.currentSession = compatibleSession } } - connect = async (uri: string) => { + async connect(uri: string) { const isValidWalletConnectUri = uri && uri.startsWith('wc') - if (isValidWalletConnectUri && this.#web3Wallet) { - await this.#web3Wallet.core.pairing.pair({ uri }) + if (isValidWalletConnectUri && this.web3Wallet) { + await this.web3Wallet.core.pairing.pair({ uri }) } } - disconnect = async () => { - if (!this.#web3Wallet || !this.#currentSession) { + async disconnect() { + if (!this.web3Wallet || !this.currentSession) { throw Error('Cannot disconnect if no session is active') } - await this.#web3Wallet.disconnectSession({ - topic: this.#currentSession.topic, + + await this.web3Wallet.disconnectSession({ + topic: this.currentSession.topic, reason: { code: USER_DISCONNECTED_CODE, message: 'User disconnected. Safe Wallet Session ended by the user', }, }) - this.#currentSession = undefined + + this.currentSession = undefined } - resetSession = () => { - this.#currentSession = undefined + resetSession() { + this.currentSession = undefined } - onSessionProposal = (handler: (proposal: Web3WalletTypes.SessionProposal) => void) => { + onSessionProposal(handler: (proposal: Web3WalletTypes.SessionProposal) => void): () => void { // events - this.#web3Wallet?.on('session_proposal', handler) - return () => this.#web3Wallet?.off('session_proposal', handler) + this.web3Wallet?.on('session_proposal', handler) + return () => this.web3Wallet?.off('session_proposal', handler) } - onSessionDelete = (handler: () => void) => { - this.#web3Wallet?.on('session_delete', handler) - return () => this.#web3Wallet?.off('session_delete', handler) + onSessionDelete(handler: () => void): () => void { + this.web3Wallet?.on('session_delete', handler) + return () => this.web3Wallet?.off('session_delete', handler) } - onSessionRequest = (handler: (event: Web3WalletTypes.SessionRequest) => void) => { + onSessionRequest(handler: (event: Web3WalletTypes.SessionRequest) => void): () => void { console.log('Registering session request handler') - this.#web3Wallet?.on('session_request', handler) - return () => this.#web3Wallet?.off('session_request', handler) + this.web3Wallet?.on('session_request', handler) + return () => this.web3Wallet?.off('session_request', handler) } - approveSessionProposal = async ( - sessionProposal: Web3WalletTypes.SessionProposal, - onMismatchingNamespaces: () => void, - ) => { - if (!this.#web3Wallet) { + async approveSessionProposal(sessionProposal: Web3WalletTypes.SessionProposal, onMismatchingNamespaces: () => void) { + if (!this.web3Wallet) { throw new Error('Web3Wallet needs to be initialized first') } + const { id, params } = sessionProposal const { requiredNamespaces } = params const requiredNamespace = requiredNamespaces[EVMBasedNamespaces] - const safeChain = `${EVMBasedNamespaces}:${this.#safe.chainId}` + const safeChain = `${EVMBasedNamespaces}:${this.safe.chainId}` const safeEvents = requiredNamespace?.events || [] // we accept all events like chainChanged & accountsChanged (even if they are not compatible with the Safe) const requiredChains = [...(requiredNamespace.chains ?? [])] // If the user accepts we always return all required namespaces and add the safe chain to it - const safeAccount = `${EVMBasedNamespaces}:${this.#safe.chainId}:${this.#safe.address.value}` + const safeAccount = `${EVMBasedNamespaces}:${this.safe.chainId}:${this.safe.address.value}` // We first pretend that our Safe is available on all required networks const safeOnRequiredChains = requiredChains.map( - (requiredChain) => `${requiredChain ?? safeChain}:${this.#safe.address.value}`, + (requiredChain) => `${requiredChain ?? safeChain}:${this.safe.address.value}`, ) let wcSession: SessionTypes.Struct try { - wcSession = await this.#web3Wallet.approveSession({ + wcSession = await this.web3Wallet.approveSession({ id, namespaces: { eip155: { @@ -185,7 +188,7 @@ export class WalletConnect { }, }) } catch (error) { - wcSession = await this.#web3Wallet.approveSession({ + wcSession = await this.web3Wallet.approveSession({ id, namespaces: { eip155: { @@ -200,7 +203,7 @@ export class WalletConnect { }) } - this.#currentSession = wcSession + this.currentSession = wcSession // Then we update the session and reduce the Safe to the requested network only if (!safeOnRequiredChains.includes(safeAccount) || safeOnRequiredChains.length > 1) { @@ -210,7 +213,7 @@ export class WalletConnect { // Emit accountsChanged and chainChanged event try { - await this.#web3Wallet.updateSession({ + await this.web3Wallet.updateSession({ topic: wcSession.topic, namespaces: { eip155: { @@ -227,29 +230,29 @@ export class WalletConnect { } } - sendSessionResponse = async (params: { topic: string; response: JsonRpcResponse }) => { - if (!this.#web3Wallet) { + async sendSessionResponse(params: { topic: string; response: JsonRpcResponse }) { + if (!this.web3Wallet) { throw new Error('Web3Wallet needs to be initialized first') } - await this.#web3Wallet.respondSessionRequest(params) + await this.web3Wallet.respondSessionRequest(params) } - updateSafeInfo = async (safe: SafeInfo) => { - this.#safe = safe + async updateSafeInfo(safe: SafeInfo) { + this.safe = safe - if (!this.#currentSession || !this.#web3Wallet) { + if (!this.currentSession || !this.web3Wallet) { return } // We have to update the active session const safeAccount = `${EVMBasedNamespaces}:${safe.chainId}:${safe.address.value}` const safeChain = `${EVMBasedNamespaces}:${safe.chainId}` - const currentNamespace = this.#currentSession.namespaces[EVMBasedNamespaces] + const currentNamespace = this.currentSession.namespaces[EVMBasedNamespaces] const chainIsSet = currentNamespace.chains?.includes(safeChain) - await this.#web3Wallet.updateSession({ - topic: this.#currentSession.topic, + await this.web3Wallet.updateSession({ + topic: this.currentSession.topic, namespaces: { eip155: { ...currentNamespace, diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index 7491ad995e..16111af897 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -71,12 +71,14 @@ const useWalletConnect = (): useWalletConnectType => { }, [safe.address.value, safe.chainId, safe, walletConnect]) useEffect(() => { - console.log('Trying to register') + console.log('WC trying to register') if (!walletConnect || !safeWalletProvider) { return } - console.log('Registering session request handler', currentSessionTopic) + console.log('WC registering session request handler', currentSessionTopic) return walletConnect.onSessionRequest(async (event) => { + console.log('WC session request', event) + const { topic, id } = event const { request, chainId: transactionChainId } = event.params @@ -131,6 +133,7 @@ const useWalletConnect = (): useWalletConnectType => { useEffect(() => { // events return walletConnect?.onSessionProposal(async (proposal) => { + console.log('WC session proposal', proposal) setSessionProposal(proposal) setWcState(WC_CONNECT_STATE.PENDING_SESSION_REQUEST) }) From e679f6336ca0bf76e797947d9f9200456aa32c60 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 19 Sep 2023 16:47:40 +0200 Subject: [PATCH 14/17] Catch errors --- .../wallet-connect/hooks/useWalletConnect.ts | 30 +++++++++++------- src/pages/connect.tsx | 31 ------------------- src/safe-wallet-provider/provider.ts | 29 +++++++++-------- .../useSafeWalletProvider.tsx | 3 +- 4 files changed, 36 insertions(+), 57 deletions(-) delete mode 100644 src/pages/connect.tsx diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index 16111af897..83448dc1a6 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -67,15 +67,18 @@ const useWalletConnect = (): useWalletConnectType => { return } - walletConnect.updateSafeInfo(safe) + walletConnect.updateSafeInfo(safe).catch((e) => setError(e.message)) }, [safe.address.value, safe.chainId, safe, walletConnect]) + // Registering WC useEffect(() => { console.log('WC trying to register') if (!walletConnect || !safeWalletProvider) { return } + console.log('WC registering session request handler', currentSessionTopic) + return walletConnect.onSessionRequest(async (event) => { console.log('WC session request', event) @@ -97,14 +100,14 @@ const useWalletConnect = (): useWalletConnectType => { try { setError(undefined) - const result = await safeWalletProvider.request(request) + + const result = await safeWalletProvider.request({ ...request, id }) + + console.log('WC result', result) + await walletConnect.sendSessionResponse({ topic, - response: { - id, - jsonrpc: '2.0', - result, - }, + response: result, }) // TODO TRACKING @@ -113,10 +116,15 @@ const useWalletConnect = (): useWalletConnectType => { setError(error?.message) const isUserRejection = error?.message?.includes?.('Transaction was rejected') const code = isUserRejection ? USER_REJECTED_REQUEST_CODE : INVALID_METHOD_ERROR_CODE - await walletConnect.sendSessionResponse({ - topic, - response: rejectResponse(id, code, error.message), - }) + + try { + await walletConnect.sendSessionResponse({ + topic, + response: rejectResponse(id, code, error.message), + }) + } catch (e) { + console.error('WC error sending session response', e) + } } }) }, [chainInfo?.chainName, safe.chainId, safeWalletProvider, walletConnect, currentSessionTopic]) diff --git a/src/pages/connect.tsx b/src/pages/connect.tsx deleted file mode 100644 index caa3a96b19..0000000000 --- a/src/pages/connect.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { NextPage } from 'next' -import Head from 'next/head' -import { useRouter } from 'next/router' -import useLastSafe from '@/hooks/useLastSafe' -import useSafeWalletConnect from '@/safe-wallet-provider/useSafeWalletConnect' - -const Connect: NextPage = () => { - const router = useRouter() - const { safe } = router.query - const lastSafe = useLastSafe() - - if (!safe && lastSafe) { - router.replace(`/connect?safe=${lastSafe}`) - } - - useSafeWalletConnect() - - return ( - <> - - {'Safe{Wallet} – Connect'} - - -
-

Connect

-
- - ) -} - -export default Connect diff --git a/src/safe-wallet-provider/provider.ts b/src/safe-wallet-provider/provider.ts index 361d930355..e44f944d55 100644 --- a/src/safe-wallet-provider/provider.ts +++ b/src/safe-wallet-provider/provider.ts @@ -28,20 +28,23 @@ export class SafeWalletProvider { } private async makeRequest(request: RpcRequest): Promise { - const { method, params = [] } = request + const { id, method, params = [] } = request + + const rpcResult = (result: unknown) => ({ + jsonrpc: '2.0', + id, + result, + }) console.log('SafeWalletProvider request', request) switch (method) { - case 'wallet_switchEthereumChain': - return true - case 'eth_accounts': - return [this.safe.safeAddress] + return rpcResult([this.safe.safeAddress]) case 'net_version': case 'eth_chainId': - return `0x${this.safe.chainId.toString(16)}` + return rpcResult(`0x${this.safe.chainId.toString(16)}`) case 'personal_sign': { const [message, address] = params as [string, string] @@ -53,7 +56,7 @@ export class SafeWalletProvider { const response = await this.sdk.signMessage(message) const signature = 'signature' in response ? response.signature : undefined - return signature || '0x' + return rpcResult(signature || '0x') } case 'eth_sign': { @@ -66,7 +69,7 @@ export class SafeWalletProvider { const response = await this.sdk.signMessage(messageHash) const signature = 'signature' in response ? response.signature : undefined - return signature || '0x' + return rpcResult(signature || '0x') } case 'eth_signTypedData': @@ -80,11 +83,10 @@ export class SafeWalletProvider { const response = await this.sdk.signTypedMessage(parsedTypedData) const signature = 'signature' in response ? response.signature : undefined - return signature || '0x' + return rpcResult(signature || '0x') } case 'eth_sendTransaction': - console.log(params) const tx = { value: '0', data: '0x', @@ -117,7 +119,8 @@ export class SafeWalletProvider { blockNumber: null, transactionIndex: null, }) - return resp.safeTxHash + + return rpcResult(resp.safeTxHash) case 'eth_getTransactionByHash': let txHash = params[0] as string @@ -127,7 +130,7 @@ export class SafeWalletProvider { } catch (e) {} // Use fake transaction if we don't have a real tx hash if (this.submittedTxs.has(txHash)) { - return this.submittedTxs.get(txHash) + return rpcResult(this.submittedTxs.get(txHash)) } return await this.sdk.proxy(method, [txHash]) @@ -148,7 +151,7 @@ export class SafeWalletProvider { async request(request: RpcRequest): Promise<{ jsonrpc: string id: number - result?: unknown + result: unknown }> { const result = await this.makeRequest(request) return { diff --git a/src/safe-wallet-provider/useSafeWalletProvider.tsx b/src/safe-wallet-provider/useSafeWalletProvider.tsx index a401913dea..70cb744aaa 100644 --- a/src/safe-wallet-provider/useSafeWalletProvider.tsx +++ b/src/safe-wallet-provider/useSafeWalletProvider.tsx @@ -80,8 +80,7 @@ const useSafeWalletProvider = (): SafeWalletProvider | undefined => { }, async proxy(method: string, params: unknown[]) { - const data = await web3ReadOnly?.send(method, params) - return data.result + return await web3ReadOnly?.send(method, params) }, } }, [safeAddress, chainId, setTxFlow, web3ReadOnly]) From 0688b366159cb97b341033656a7fac6fe845721e Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 20 Sep 2023 08:37:37 +0200 Subject: [PATCH 15/17] Catch error on disconnect --- .../wallet-connect/hooks/useWalletConnect.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index 83448dc1a6..2f2d40c674 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -164,13 +164,18 @@ const useWalletConnect = (): useWalletConnectType => { ) const wcApproveSession = useCallback(async () => { - if (!sessionProposal || !walletConnect) { - throw new Error('Cannot approve session without pending session proposal') - } + try { + if (!sessionProposal || !walletConnect) { + throw new Error('Cannot approve session without pending session proposal') + } - await walletConnect.approveSessionProposal(sessionProposal, () => { - setWcState(WC_CONNECT_STATE.APPROVE_INVALID_SESSION) - }) + await walletConnect.approveSessionProposal(sessionProposal, () => { + setWcState(WC_CONNECT_STATE.APPROVE_INVALID_SESSION) + }) + } catch (e) { + setError((e as Error).message) + return + } setSessionProposal(undefined) setError(undefined) From 5eb5938563534b0a1320ff8521b09e1a1237b0be Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 20 Sep 2023 12:01:43 +0200 Subject: [PATCH 16/17] Extract constants --- .../wallet-connect/hooks/WalletConnect.ts | 49 ++----------------- .../wallet-connect/hooks/constants.ts | 42 ++++++++++++++++ 2 files changed, 47 insertions(+), 44 deletions(-) create mode 100644 src/components/wallet-connect/hooks/constants.ts diff --git a/src/components/wallet-connect/hooks/WalletConnect.ts b/src/components/wallet-connect/hooks/WalletConnect.ts index ab2dd6e07c..943ff1cf56 100644 --- a/src/components/wallet-connect/hooks/WalletConnect.ts +++ b/src/components/wallet-connect/hooks/WalletConnect.ts @@ -6,49 +6,10 @@ import { type JsonRpcResponse } from '@walletconnect/jsonrpc-utils' import type Web3WalletType from '@walletconnect/web3wallet' import { type SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import { type SessionTypes } from 'walletconnect-v2-types' +import { EVMBasedNamespaces, SAFE_COMPATIBLE_METHODS, SAFE_WALLET_METADATA, WC_ERRORS } from './constants' const logger = IS_PRODUCTION ? undefined : 'debug' -const USER_DISCONNECTED_CODE = 6000 - -const EVMBasedNamespaces: string = 'eip155' - -// see full list here: https://github.com/safe-global/safe-apps-sdk/blob/main/packages/safe-apps-provider/src/provider.ts#L35 -export const compatibleSafeMethods: string[] = [ - 'eth_accounts', - 'net_version', - 'eth_chainId', - 'personal_sign', - 'eth_sign', - 'eth_signTypedData', - 'eth_signTypedData_v4', - 'eth_sendTransaction', - 'eth_blockNumber', - 'eth_getBalance', - 'eth_getCode', - 'eth_getTransactionCount', - 'eth_getStorageAt', - 'eth_getBlockByNumber', - 'eth_getBlockByHash', - 'eth_getTransactionByHash', - 'eth_getTransactionReceipt', - 'eth_estimateGas', - 'eth_call', - 'eth_getLogs', - 'eth_gasPrice', - 'wallet_getPermissions', - 'wallet_requestPermissions', - 'safe_setSettings', -] - -// MOVE TO CONSTANTS -export const SAFE_WALLET_METADATA = { - name: 'Safe Wallet', - description: 'The most trusted platform to manage digital assets on Ethereum', - url: 'https://app.safe.global', - icons: ['https://app.safe.global/favicons/mstile-150x150.png', 'https://app.safe.global/favicons/logo_120x120.png'], -} - export class WalletConnect { private web3Wallet: Web3WalletType | undefined private safe: SafeInfo @@ -124,7 +85,7 @@ export class WalletConnect { await this.web3Wallet.disconnectSession({ topic: this.currentSession.topic, reason: { - code: USER_DISCONNECTED_CODE, + code: WC_ERRORS.USER_DISCONNECTED_CODE, message: 'User disconnected. Safe Wallet Session ended by the user', }, }) @@ -182,7 +143,7 @@ export class WalletConnect { eip155: { accounts: [safeAccount], // only the Safe account chains: [safeChain], // only the Safe chain - methods: compatibleSafeMethods, // only the Safe methods + methods: SAFE_COMPATIBLE_METHODS, // only the Safe methods events: safeEvents, }, }, @@ -196,7 +157,7 @@ export class WalletConnect { ? safeOnRequiredChains : [safeAccount, ...safeOnRequiredChains], // Add all required chains on top chains: requiredChains, // return the required Safes - methods: compatibleSafeMethods, // only the Safe methods + methods: SAFE_COMPATIBLE_METHODS, // only the Safe methods events: safeEvents, }, }, @@ -219,7 +180,7 @@ export class WalletConnect { eip155: { accounts: [safeAccount], chains: requiredChains, - methods: compatibleSafeMethods, + methods: SAFE_COMPATIBLE_METHODS, events: safeEvents, }, }, diff --git a/src/components/wallet-connect/hooks/constants.ts b/src/components/wallet-connect/hooks/constants.ts new file mode 100644 index 0000000000..34857a39f9 --- /dev/null +++ b/src/components/wallet-connect/hooks/constants.ts @@ -0,0 +1,42 @@ +export const SAFE_COMPATIBLE_METHODS = [ + 'eth_accounts', + 'net_version', + 'eth_chainId', + 'personal_sign', + 'eth_sign', + 'eth_signTypedData', + 'eth_signTypedData_v4', + 'eth_sendTransaction', + 'eth_blockNumber', + 'eth_getBalance', + 'eth_getCode', + 'eth_getTransactionCount', + 'eth_getStorageAt', + 'eth_getBlockByNumber', + 'eth_getBlockByHash', + 'eth_getTransactionByHash', + 'eth_getTransactionReceipt', + 'eth_estimateGas', + 'eth_call', + 'eth_getLogs', + 'eth_gasPrice', + 'wallet_getPermissions', + 'wallet_requestPermissions', + 'safe_setSettings', +] + +export enum WC_ERRORS { + UNSUPPORTED_CHAIN_ERROR_CODE = 5100, + INVALID_METHOD_ERROR_CODE = 1001, + USER_REJECTED_REQUEST_CODE = 4001, + USER_DISCONNECTED_CODE = 6000, +} + +export const SAFE_WALLET_METADATA = { + name: 'Safe Wallet', + description: 'The most trusted platform to manage digital assets on Ethereum', + url: 'https://app.safe.global', + icons: ['https://app.safe.global/favicons/mstile-150x150.png', 'https://app.safe.global/favicons/logo_120x120.png'], +} + +export const EVMBasedNamespaces = 'eip155' From 8cc2924d9dd3c22d1f139e3c399e77bd767f0fc4 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 20 Sep 2023 12:05:21 +0200 Subject: [PATCH 17/17] Rm onPaste --- .../wallet-connect/hooks/useWalletConnect.ts | 12 ++--- src/components/wallet-connect/index.tsx | 44 ++----------------- 2 files changed, 7 insertions(+), 49 deletions(-) diff --git a/src/components/wallet-connect/hooks/useWalletConnect.ts b/src/components/wallet-connect/hooks/useWalletConnect.ts index 2f2d40c674..79dc5b9603 100644 --- a/src/components/wallet-connect/hooks/useWalletConnect.ts +++ b/src/components/wallet-connect/hooks/useWalletConnect.ts @@ -5,13 +5,7 @@ import { useCurrentChain } from '@/hooks/useChains' import useSafeInfo from '@/hooks/useSafeInfo' import useSafeWalletProvider from '@/safe-wallet-provider/useSafeWalletProvider' import { WalletConnect } from './WalletConnect' - -const EVMBasedNamespaces: string = 'eip155' - -// see https://docs.walletconnect.com/2.0/specs/sign/error-codes -const UNSUPPORTED_CHAIN_ERROR_CODE = 5100 -const INVALID_METHOD_ERROR_CODE = 1001 -const USER_REJECTED_REQUEST_CODE = 4001 +import { WC_ERRORS, EVMBasedNamespaces } from './constants' export const errorLabel = 'We were unable to create a connection due to compatibility issues with the latest WalletConnect v2 upgrade. We are actively working with the WalletConnect team and the dApps to get these issues resolved. Use Safe Apps instead wherever possible.' @@ -93,7 +87,7 @@ const useWalletConnect = (): useWalletConnectType => { setError(errorMessage) await walletConnect.sendSessionResponse({ topic, - response: rejectResponse(id, UNSUPPORTED_CHAIN_ERROR_CODE, errorMessage), + response: rejectResponse(id, WC_ERRORS.UNSUPPORTED_CHAIN_ERROR_CODE, errorMessage), }) return } @@ -115,7 +109,7 @@ const useWalletConnect = (): useWalletConnectType => { } catch (error: any) { setError(error?.message) const isUserRejection = error?.message?.includes?.('Transaction was rejected') - const code = isUserRejection ? USER_REJECTED_REQUEST_CODE : INVALID_METHOD_ERROR_CODE + const code = isUserRejection ? WC_ERRORS.USER_REJECTED_REQUEST_CODE : WC_ERRORS.INVALID_METHOD_ERROR_CODE try { await walletConnect.sendSessionResponse({ diff --git a/src/components/wallet-connect/index.tsx b/src/components/wallet-connect/index.tsx index 8ad3b45c43..1bc5b7a496 100644 --- a/src/components/wallet-connect/index.tsx +++ b/src/components/wallet-connect/index.tsx @@ -13,7 +13,7 @@ import { Typography, } from '@mui/material' import WcIcon from '@/public/images/apps/wallet-connect.svg' -import { type ChangeEvent, useCallback, useState, useRef } from 'react' +import { type ChangeEvent, useState, useRef } from 'react' import useWalletConnect, { WC_CONNECT_STATE } from './hooks/useWalletConnect' import css from './styles.module.css' import type { Web3WalletTypes } from '@walletconnect/web3wallet' @@ -38,7 +38,6 @@ const extractInformationFromProposal = (sessionProposal: Web3WalletTypes.Session export const ConnectWC = () => { const [openModal, setOpenModal] = useState(false) - const [wcConnectUrl, setWcConnectUrl] = useState('') const { wcConnect, wcDisconnect, wcClientData, wcApproveSession, acceptInvalidSession, wcState, sessionProposal } = useWalletConnect() const anchorElem = useRef(null) @@ -55,45 +54,11 @@ export const ConnectWC = () => { setOpenModal((prev) => !prev) } - const onConnect = useCallback( - async (uri: string) => { - await wcConnect(uri) - }, - [wcConnect], - ) - - const onChangeWcUrl = (event: ChangeEvent) => { - const newValue = event.target.value - setWcConnectUrl(newValue) + const onChangeWcUrl = async (event: ChangeEvent) => { + const uri = event.target.value + await wcConnect(uri) } - const onPaste = useCallback( - (event: React.ClipboardEvent) => { - const connectWithUri = (data: string) => { - if (data.startsWith('wc')) { - onConnect(data) - } - } - - setWcConnectUrl('') - - if (wcClientData) { - return - } - - const items = event.clipboardData.items - - for (const index in items) { - const item = items[index] - - if (item.kind === 'string' && item.type === 'text/plain') { - connectWithUri(event.clipboardData.getData('Text')) - } - } - }, - [wcClientData, onConnect], - ) - return ( <> @@ -219,7 +184,6 @@ export const ConnectWC = () => {