From ad534338c88fe4928f0506f0d201ea0f7c6597f2 Mon Sep 17 00:00:00 2001 From: Hayden Briese Date: Mon, 21 Aug 2023 16:14:40 +1000 Subject: [PATCH] Handle walletconnect signing requests --- .../useSessionRequestListener.ts | 145 +++++++++++++----- app/src/util/walletconnect/types.ts | 6 - 2 files changed, 106 insertions(+), 45 deletions(-) diff --git a/app/src/components/walletconnect/useSessionRequestListener.ts b/app/src/components/walletconnect/useSessionRequestListener.ts index c0a1e2366..82cc7d4e2 100644 --- a/app/src/components/walletconnect/useSessionRequestListener.ts +++ b/app/src/components/walletconnect/useSessionRequestListener.ts @@ -1,22 +1,23 @@ import { gql } from '@api/generated'; import { useNavigation } from '@react-navigation/native'; -import { Hex, asBigInt } from 'lib'; -import { useCallback } from 'react'; +import { asBigInt } from 'lib'; +import { useEffect, useState } from 'react'; import { showInfo } from '~/provider/SnackbarProvider'; import { logError } from '~/util/analytics'; -import { WalletConnectEventArgs, WcClient, asWalletConnectResult } from '~/util/walletconnect'; +import { asWalletConnectResult, useWalletConnectWithoutWatching } from '~/util/walletconnect'; import { SigningRequest, WC_SIGNING_METHODS, WC_TRANSACTION_METHODS, WalletConnectSendTransactionRequest, + normalizeSigningRequest, } from '~/util/walletconnect/methods'; -import { EventEmitter } from '~/util/EventEmitter'; import { usePropose } from '@api/usePropose'; -import { useQuery } from '~/gql'; -import { useSubscription } from 'urql'; - -const PROPOSAL_EXECUTED_EMITTER = new EventEmitter(); +import { getOptimizedDocument, useQuery } from '~/gql'; +import { Subject } from 'rxjs'; +import { SignClientTypes } from '@walletconnect/types'; +import { useMutation, useSubscription } from 'urql'; +import { SessionRequestListener_ProposalSubscription } from '@api/generated/graphql'; const Query = gql(/* GraphQL */ ` query UseSessionRequestListener { @@ -27,42 +28,66 @@ const Query = gql(/* GraphQL */ ` } `); -const Subscription = gql(/* GraphQL */ ` - subscription SessionRequestListener($accounts: [Address!]!) { - proposal(input: { accounts: $accounts, events: [executed] }) { +const ProposeMessage = gql(/* GraphQL */ ` + mutation UseSessionRequestListener_ProposeMessage($input: ProposeMessageInput!) { + proposeMessage(input: $input) { id hash } } `); +const ProposalSubscription = gql(/* GraphQL */ ` + subscription SessionRequestListener_Proposal($accounts: [Address!]!) { + proposal(input: { accounts: $accounts, events: [approved, executed] }) { + __typename + id + hash + ... on TransactionProposal { + transaction { + id + hash + } + } + ... on MessageProposal { + signature + } + } + } +`); + +type SessionRequestArgs = SignClientTypes.EventArguments['session_request']; + export const useSessionRequestListener = () => { const { navigate } = useNavigation(); - const propose = usePropose(); + const proposeTransaction = usePropose(); + const proposeMessage = useMutation(ProposeMessage)[1]; + const client = useWalletConnectWithoutWatching(); + + const accounts = useQuery(Query).data.accounts.map((a) => a.address); + + const [proposals] = useState( + new Subject(), + ); + useEffect(() => proposals.unsubscribe, [proposals]); - const { accounts } = useQuery(Query).data; - useSubscription({ - query: Subscription, - variables: { accounts: accounts.map((a) => a.address) }, - }); + useSubscription( + { query: getOptimizedDocument(ProposalSubscription), variables: { accounts } }, + (_prev, data) => { + proposals.next(data.proposal); + return data; + }, + ); - return useCallback( - async (client: WcClient, { id, topic, params }: WalletConnectEventArgs['session_request']) => { + useEffect(() => { + const handleRequest = async ({ id, topic, params }: SessionRequestArgs) => { const method = params.request.method; + const peer = client.session.get(topic).peer.metadata; - if (WC_SIGNING_METHODS.has(method)) { - navigate('Sign', { - topic, - id, - request: params.request as SigningRequest, - }); - } else if (WC_TRANSACTION_METHODS.has(method)) { + if (WC_TRANSACTION_METHODS.has(method)) { const [tx] = (params.request as WalletConnectSendTransactionRequest).params; - const peer = client.session.get(topic).peer.metadata; - showInfo(`${peer.name} has proposed a transaction`); - - const proposal = await propose({ + const proposal = await proposeTransaction({ account: tx.from, operations: [ { @@ -74,20 +99,62 @@ export const useSessionRequestListener = () => { gasLimit: tx.gasLimit ? asBigInt(tx.gasLimit) : undefined, }); - PROPOSAL_EXECUTED_EMITTER.listeners.add((proposalHash) => { - if (proposalHash === proposal) { - client.respond({ - topic, - response: asWalletConnectResult(id, proposalHash), - }); + showInfo(`${peer.name} has proposed a transaction`); + + const sub = proposals.subscribe((p) => { + if ( + p.hash === proposal && + p.__typename === 'TransactionProposal' && + p.transaction?.hash + ) { + client.respond({ topic, response: asWalletConnectResult(id, p.transaction.hash) }); + sub.unsubscribe(); } }); navigate('Proposal', { proposal }); + + // sub is automatically unsubscribed due to proposalsExecuted unsubscribe on unmount + } else if (WC_SIGNING_METHODS.has(method)) { + const request = params.request as SigningRequest; + const { account, message } = normalizeSigningRequest(request); + + // TODO: handle EIP712 messages + if (typeof message !== 'string') throw new Error('EIP712 signing unimplemented!'); + + const proposal = ( + await proposeMessage({ + input: { + account, + message, + label: `${peer.name} signature request`, + iconUri: peer.icons[0], + }, + }) + ).data?.proposeMessage.hash; + if (!proposal) return; + + showInfo(`${peer.name} has requested a signature`); + + const sub = proposals.subscribe((p) => { + if (p.hash === proposal && p.__typename === 'MessageProposal' && p.signature) { + client.respond({ topic, response: asWalletConnectResult(id, p.signature) }); + sub.unsubscribe(); + } + }); + + navigate('MessageProposal', { proposal }); + + // sub is automatically unsubscribed due to proposalsApproved unsubscribe on unmount } else { logError('Unsupported session_request method executed', { params }); } - }, - [navigate, propose], - ); + }; + + client.on('session_request', handleRequest); + + return () => { + client.off('session_request', handleRequest); + }; + }, [client, navigate, proposals, proposeMessage, proposeTransaction]); }; diff --git a/app/src/util/walletconnect/types.ts b/app/src/util/walletconnect/types.ts index ce7e48eb5..8da0dce3d 100644 --- a/app/src/util/walletconnect/types.ts +++ b/app/src/util/walletconnect/types.ts @@ -1,12 +1,6 @@ -import { SignClientTypes, SessionTypes } from '@walletconnect/types'; import { getSdkError } from '@walletconnect/utils'; import { formatJsonRpcResult, formatJsonRpcError } from '@walletconnect/jsonrpc-utils'; -export type WalletConnectEvent = SignClientTypes.Event; -export type WalletConnectEventArgs = SignClientTypes.EventArguments; -export type WalletConnectPeer = SignClientTypes.Metadata; -export type WalletConnectSession = SessionTypes.Struct; - export const asWalletConnectResult = formatJsonRpcResult; export type WalletConnectErrorKey = Parameters[0];