From 0c00f6a2ad7d234feea5219dc206c0ddb2a481c7 Mon Sep 17 00:00:00 2001 From: Skylar Barrera Date: Tue, 19 Mar 2024 12:17:14 -0400 Subject: [PATCH] wc: generalize tx request sheet (#5471) * save * bulk of the work * clean up * clean up and add browser request handler * bug * add initial connection sheet --- src/components/coin-row/RequestCoinRow.js | 10 +- .../buildTransactionsSectionsSelector.tsx | 4 +- src/navigation/config.tsx | 7 +- src/redux/requests.ts | 47 ++- src/redux/walletconnect.ts | 22 +- src/screens/SignTransactionSheet.tsx | 283 +++++------------- src/screens/WalletConnectApprovalSheet.js | 2 +- src/utils/requestNavigationHandlers.ts | 178 +++++++++++ src/walletConnect/index.tsx | 24 +- 9 files changed, 310 insertions(+), 267 deletions(-) create mode 100644 src/utils/requestNavigationHandlers.ts diff --git a/src/components/coin-row/RequestCoinRow.js b/src/components/coin-row/RequestCoinRow.js index dfd59520d47..6adbe5b0b29 100644 --- a/src/components/coin-row/RequestCoinRow.js +++ b/src/components/coin-row/RequestCoinRow.js @@ -9,10 +9,9 @@ import { RowWithMargins } from '../layout'; import { Emoji, Text } from '../text'; import CoinName from './CoinName'; import CoinRow from './CoinRow'; -import { useNavigation } from '@/navigation'; import { removeRequest } from '@/redux/requests'; -import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; +import { handleWalletConnectRequest } from '@/utils/requestNavigationHandlers'; const getPercentageOfTimeElapsed = (startDate, endDate) => { const originalDifference = differenceInMinutes(endDate, startDate); @@ -50,7 +49,6 @@ const TopRow = ({ expirationColor, expiresAt }) => { const RequestCoinRow = ({ item, ...props }) => { const buttonRef = useRef(); const dispatch = useDispatch(); - const { navigate } = useNavigation(); const [expiresAt, setExpiresAt] = useState(null); const [expirationColor, setExpirationColor] = useState(null); const [percentElapsed, setPercentElapsed] = useState(null); @@ -74,10 +72,8 @@ const RequestCoinRow = ({ item, ...props }) => { }, [dispatch, expiresAt, item.requestId]); const handlePressOpen = useCallback(() => { - navigate(Routes.CONFIRM_REQUEST, { - transactionDetails: item, - }); - }, [item, navigate]); + handleWalletConnectRequest(item); + }, [item]); useEffect(() => { handleExpiredRequests(); diff --git a/src/helpers/buildTransactionsSectionsSelector.tsx b/src/helpers/buildTransactionsSectionsSelector.tsx index 872ff9e5ec7..86e76a100fb 100644 --- a/src/helpers/buildTransactionsSectionsSelector.tsx +++ b/src/helpers/buildTransactionsSectionsSelector.tsx @@ -5,7 +5,7 @@ import { FastTransactionCoinRow, RequestCoinRow } from '../components/coin-row'; import { thisMonthTimestamp, thisYearTimestamp, todayTimestamp, yesterdayTimestamp } from './transactions'; import { NativeCurrencyKey, RainbowTransaction, TransactionStatusTypes } from '@/entities'; import * as i18n from '@/languages'; -import { RequestData } from '@/redux/requests'; +import { WalletconnectRequestData } from '@/redux/requests'; import { ThemeContextProps } from '@/theme'; import { Contact } from '@/redux/contacts'; import { TransactionStatus } from '@/resources/transactions/types'; @@ -63,7 +63,7 @@ export const buildTransactionsSections = ({ }: { accountAddress: string; contacts: { [address: string]: Contact }; - requests: RequestData[]; + requests: WalletconnectRequestData[]; theme: ThemeContextProps; transactions: RainbowTransaction[]; nativeCurrency: NativeCurrencyKey; diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 7aadef88868..32d5a600202 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -27,6 +27,7 @@ import { HARDWARE_WALLET_TX_NAVIGATOR_SHEET_HEIGHT } from '@/navigation/Hardware import { StackNavigationOptions } from '@react-navigation/stack'; import { PartialNavigatorConfigOptions } from '@/navigation/types'; import { BottomSheetNavigationOptions } from '@/navigation/bottom-sheet/types'; +import { SignTransactionSheetRouteProp } from '@/screens/SignTransactionSheet'; export const sharedCoolModalTopOffset = safeAreaInsetValues.top; @@ -228,10 +229,10 @@ export const swapConfig = { }; export const signTransactionSheetConfig = { - options: ({ route: { params = {} } }) => ({ + options: ({ route }: { route: SignTransactionSheetRouteProp }) => ({ ...buildCoolModalConfig({ - ...params, - backgroundOpacity: 1, + ...route.params, + backgroundOpacity: route?.params?.requestType === 'walletconnect' ? 1 : 0.3, cornerRadius: 0, springDamping: 1, topOffset: 0, diff --git a/src/redux/requests.ts b/src/redux/requests.ts index 280d992f3aa..4dba3b81dda 100644 --- a/src/redux/requests.ts +++ b/src/redux/requests.ts @@ -7,6 +7,7 @@ import { omitFlatten } from '@/helpers/utilities'; import { getRequestDisplayDetails } from '@/parsers'; import { ethereumUtils } from '@/utils'; import logger from '@/utils/logger'; +import { Network } from '@/networks/types'; // -- Constants --------------------------------------- // @@ -18,10 +19,19 @@ const EXPIRATION_THRESHOLD_IN_MS = 1000 * 60 * 60; // -- Actions ----------------------------------------- // +export interface RequestData { + dappName: string; + imageUrl: string | undefined; + address: string; + network: Network; + dappUrl: string; + payload: any; + displayDetails: RequestDisplayDetails | null | Record; +} /** * A request stored in state. */ -export interface RequestData { +export interface WalletconnectRequestData extends RequestData { /** * The WalletConnect client ID for the request. */ @@ -37,36 +47,16 @@ export interface RequestData { */ requestId: number; - /** - * The name of the dapp the user is connecting to. - */ - dappName: string; - /** * The URL scheme to use for re-opening the dapp, or null. */ dappScheme: string | null; - /** - * The URL for the dapp. - */ - dappUrl: string; - /** * The display details loaded for the request. */ displayDetails: RequestDisplayDetails | null | Record; - /** - * The image URL for the dapp, or undefined. - */ - imageUrl: string | undefined; - - /** - * The payload for the request. - */ - payload: any; - /** * Adds additional data to the request and serves as a notice that this * request originated from a WC v2 session @@ -75,7 +65,7 @@ export interface RequestData { sessionRequestEvent: SignClientTypes.EventArguments['session_request']; address: string; chainId: number; - onComplete(type: string): void; + onComplete?: (type: string) => void; }; } @@ -99,11 +89,11 @@ interface RequestDisplayDetails { */ interface RequestsState { /** - * Current requests, as an object mapping request IDs to `RequestData` + * Current requests, as an object mapping request IDs to `WalletconnectRequestData` * objects. */ requests: { - [requestId: number]: RequestData; + [requestId: number]: WalletconnectRequestData; }; } @@ -172,6 +162,9 @@ export const addRequestToApprove = const walletConnector = walletConnectors[peerId]; // @ts-expect-error "_chainId" is private. const chainId = walletConnector._chainId; + const requestNetwork = ethereumUtils.getNetworkFromChainId(Number(chainId)); + // @ts-expect-error "_accounts" is private. + const address = walletConnector._accounts[0]; const dappNetwork = ethereumUtils.getNetworkFromChainId(Number(chainId)); const displayDetails = getRequestDisplayDetails(payload, nativeCurrency, dappNetwork); const oneHourAgoTs = Date.now() - EXPIRATION_THRESHOLD_IN_MS; @@ -189,7 +182,9 @@ export const addRequestToApprove = const dappUrl = peerMeta?.url || 'Unknown Url'; const dappScheme = peerMeta?.scheme || null; - const request: RequestData = { + const request: WalletconnectRequestData = { + address, + network: requestNetwork, clientId, dappName, dappScheme, @@ -217,7 +212,7 @@ export const addRequestToApprove = */ export const requestsForTopic = (topic: string | undefined) => - (dispatch: unknown, getState: AppGetState): RequestData[] => { + (dispatch: unknown, getState: AppGetState): WalletconnectRequestData[] => { const { requests } = getState().requests; return Object.values(requests).filter(({ clientId }) => clientId === topic); }; diff --git a/src/redux/walletconnect.ts b/src/redux/walletconnect.ts index e4f493d971e..c86b0f05f64 100644 --- a/src/redux/walletconnect.ts +++ b/src/redux/walletconnect.ts @@ -16,7 +16,7 @@ import { sendRpcCall } from '../handlers/web3'; import WalletTypes from '../helpers/walletTypes'; import { Navigation } from '../navigation'; import { isSigningMethod } from '../utils/signingMethods'; -import { addRequestToApprove, RequestData } from './requests'; +import { addRequestToApprove, WalletconnectRequestData } from './requests'; import { AppGetState, AppState as StoreAppState } from './store'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { analytics } from '@/analytics'; @@ -31,6 +31,7 @@ import { logger, RainbowError } from '@/logger'; import { IS_DEV, IS_IOS, IS_TEST } from '@/env'; import { RainbowNetworks } from '@/networks'; import { Verify } from '@walletconnect/types'; +import { handleWalletConnectRequest } from '@/utils/requestNavigationHandlers'; // -- Variables --------------------------------------- // let showRedirectSheetThreshold = 300; @@ -133,10 +134,10 @@ export interface WalletconnectApprovalSheetRouteParams { approved: boolean, chainId: number, accountAddress: string, - peerId: RequestData['peerId'], - dappScheme: RequestData['dappScheme'], - dappName: RequestData['dappName'], - dappUrl: RequestData['dappUrl'] + peerId: WalletconnectRequestData['peerId'], + dappScheme: WalletconnectRequestData['dappScheme'], + dappName: WalletconnectRequestData['dappName'], + dappUrl: WalletconnectRequestData['dappUrl'] ) => Promise; receivedTimestamp: number; meta?: { @@ -148,7 +149,7 @@ export interface WalletconnectApprovalSheetRouteParams { */ chainIds: number[]; isWalletConnectV2?: boolean; - } & Pick; + } & Pick; timeout?: ReturnType | null; timedOut?: boolean; failureExplainSheetVariant?: string; @@ -557,10 +558,7 @@ const listenOnNewMessages = const { requests: pendingRequests } = getState().requests; const request = !pendingRequests[requestId] ? dispatch(addRequestToApprove(clientId, peerId, requestId, payload, peerMeta)) : null; if (request) { - Navigation.handleAction(Routes.CONFIRM_REQUEST, { - openAutomatically: true, - transactionDetails: request, - }); + handleWalletConnectRequest(request); InteractionManager.runAfterInteractions(() => { analytics.track('Showing Walletconnect signing request', { dappName, @@ -741,7 +739,7 @@ export const removeWalletConnector = * @param chainId The chain ID to use. */ export const walletConnectUpdateSessionConnectorByDappUrl = - (dappUrl: RequestData['dappUrl'], accountAddress: string, chainId: number) => + (dappUrl: WalletconnectRequestData['dappUrl'], accountAddress: string, chainId: number) => (dispatch: Dispatch, getState: AppGetState) => { const { walletConnectors } = getState().walletconnect; const connectors = pickBy(walletConnectors, connector => { @@ -774,7 +772,7 @@ export const walletConnectApproveSession = ( peerId: string, callback: WalletconnectRequestCallback | undefined, - dappScheme: RequestData['dappScheme'], + dappScheme: WalletconnectRequestData['dappScheme'], chainId: number, accountAddress: string ) => diff --git a/src/screens/SignTransactionSheet.tsx b/src/screens/SignTransactionSheet.tsx index bdd79712287..bcf3749f055 100644 --- a/src/screens/SignTransactionSheet.tsx +++ b/src/screens/SignTransactionSheet.tsx @@ -33,7 +33,7 @@ import { useNavigation } from '@/navigation'; import { useTheme } from '@/theme'; import { abbreviations, ethereumUtils, safeAreaInsetValues } from '@/utils'; import { PanGestureHandler } from 'react-native-gesture-handler'; -import { useIsFocused, useRoute } from '@react-navigation/native'; +import { RouteProp, useRoute } from '@react-navigation/native'; import { metadataPOSTClient } from '@/graphql'; import { TransactionAssetType, @@ -54,8 +54,7 @@ import { greaterThanOrEqualTo, omitFlatten, } from '@/helpers/utilities'; -import { useDispatch, useSelector } from 'react-redux'; -import { AppState } from '../redux/store'; + import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import { getAccountProfileInfo } from '@/helpers/accountInfo'; import { useAccountSettings, useClipboard, useDimensions, useGas, useWallets } from '@/hooks'; @@ -83,9 +82,6 @@ import { parseGasParamsForTransaction } from '@/parsers/gas'; import { loadWallet, sendTransaction, signPersonalMessage, signTransaction, signTypedDataMessage } from '@/model/wallet'; import { analytics } from '@/analytics'; -import { handleSessionRequestResponse } from '@/walletConnect'; -import { WalletconnectResultType, walletConnectRemovePendingRedirect, walletConnectSendStatus } from '@/redux/walletconnect'; -import { removeRequest } from '@/redux/requests'; import { maybeSignUri } from '@/handlers/imgix'; import { RPCMethod } from '@/walletConnect/types'; import { isAddress } from '@ethersproject/address'; @@ -96,6 +92,8 @@ import { addNewTransaction } from '@/state/pendingTransactions'; import { getNextNonce } from '@/state/nonces'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; +import { RequestData } from '@/redux/requests'; +import { RequestType } from '@/utils/requestNavigationHandlers'; const COLLAPSED_CARD_HEIGHT = 56; const MAX_CARD_HEIGHT = 176; @@ -129,6 +127,18 @@ const timingConfig = { easing: Easing.bezier(0.2, 0, 0, 1), }; +type SignTransactionSheetParams = { + transactionDetails: RequestData; + onSuccess: (hash: string) => void; + onCancel: (error?: Error) => void; + onCloseScreen: (canceled: boolean) => void; + network: Network; + address: string; + requestType: RequestType; +}; + +export type SignTransactionSheetRouteProp = RouteProp<{ SignTransactionSheet: SignTransactionSheetParams }, 'SignTransactionSheet'>; + export const SignTransactionSheet = () => { const { goBack, navigate } = useNavigation(); const { colors, isDarkMode } = useTheme(); @@ -136,9 +146,19 @@ export const SignTransactionSheet = () => { const [simulationData, setSimulationData] = useState(); const [simulationError, setSimulationError] = useState(undefined); const [simulationScanResult, setSimulationScanResult] = useState(undefined); - const { params: routeParams } = useRoute(); + + const { params: routeParams } = useRoute(); const { wallets, walletNames, switchToWalletWithAddress } = useWallets(); - const { callback, transactionDetails } = routeParams; + const { + transactionDetails, + onSuccess: onSuccessCallback, + onCancel: onCancelCallback, + onCloseScreen: onCloseScreenCallback, + network: currentNetwork, + address: currentAddress, + // for request type specific handling + requestType, + } = routeParams; const isMessageRequest = isMessageDisplayType(transactionDetails.payload.method); @@ -147,12 +167,7 @@ export const SignTransactionSheet = () => { const label = useForegroundColor('label'); const surfacePrimary = useBackgroundColor('surfacePrimary'); - const pendingRedirect = useSelector(({ walletconnect }: AppState) => walletconnect.pendingRedirect); - const walletConnectors = useSelector(({ walletconnect }: AppState) => walletconnect.walletConnectors); - const walletConnector = walletConnectors[transactionDetails?.peerId]; - const [provider, setProvider] = useState(null); - const [currentNetwork, setCurrentNetwork] = useState(); const [isAuthorizing, setIsAuthorizing] = useState(false); const [isLoading, setIsLoading] = useState(!isPersonalSign); const [methodName, setMethodName] = useState(null); @@ -160,9 +175,6 @@ export const SignTransactionSheet = () => { const [isBalanceEnough, setIsBalanceEnough] = useState(); const [nonceForDisplay, setNonceForDisplay] = useState(); - const isFocused = useIsFocused(); - const dispatch = useDispatch(); - const [nativeAsset, setNativeAsset] = useState(null); const formattedDappUrl = useMemo(() => { try { @@ -193,12 +205,12 @@ export const SignTransactionSheet = () => { const req = transactionDetails?.payload?.params?.[0]; const request = useMemo(() => { return isMessageRequest - ? { message: transactionDetails?.displayDetails.request } + ? { message: transactionDetails?.displayDetails?.request } : { - ...transactionDetails?.displayDetails.request, + ...transactionDetails?.displayDetails?.request, nativeAsset: nativeAsset, }; - }, [isMessageRequest, transactionDetails?.displayDetails.request, nativeAsset]); + }, [isMessageRequest, transactionDetails?.displayDetails?.request, nativeAsset]); const calculateGasLimit = useCallback(async () => { calculatingGasLimit.current = true; @@ -209,15 +221,7 @@ export const SignTransactionSheet = () => { // use the default let gas = txPayload.gasLimit || txPayload.gas; - // sometimes provider is undefined, this is hack to ensure its defined - const localCurrentNetwork = ethereumUtils.getNetworkFromChainId( - Number( - transactionDetails?.walletConnectV2RequestValues?.chainId || - // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._chainId - ) - ); - const provider = await getProviderForNetwork(localCurrentNetwork); + const provider = await getProviderForNetwork(currentNetwork); try { // attempt to re-run estimation logger.debug('WC: Estimating gas limit', { gas }, logger.DebugContext.walletconnect); @@ -240,20 +244,13 @@ export const SignTransactionSheet = () => { updateTxFee(gas, null); } } - }, [ - currentNetwork, - req, - transactionDetails?.walletConnectV2RequestValues?.chainId, - updateTxFee, - // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._chainId, - ]); + }, [currentNetwork, req, updateTxFee]); const fetchMethodName = useCallback( async (data: string) => { const methodSignaturePrefix = data.substr(0, 10); try { - const { name } = await methodRegistryLookupAndParse(methodSignaturePrefix, getNetworkObj(currentNetwork!).id); + const { name } = await methodRegistryLookupAndParse(methodSignaturePrefix, getNetworkObj(currentNetwork).id); if (name) { setMethodName(name); } @@ -327,72 +324,45 @@ export const SignTransactionSheet = () => { }, [isMessageRequest, isSufficientGas, currentNetwork, selectedGasFee, walletBalance, req]); const accountInfo = useMemo(() => { - // TODO where do we get address for sign/send transaction? - const address = - transactionDetails?.walletConnectV2RequestValues?.address || - // @ts-expect-error Property '_accounts' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._accounts?.[0]; - const selectedWallet = findWalletWithAccount(wallets!, address); - const profileInfo = getAccountProfileInfo(selectedWallet, walletNames, address); + const selectedWallet = findWalletWithAccount(wallets!, currentAddress); + const profileInfo = getAccountProfileInfo(selectedWallet, walletNames, currentAddress); return { ...profileInfo, - address, + address: currentAddress, isHardwareWallet: !!selectedWallet?.deviceId, }; - }, [ - transactionDetails?.walletConnectV2RequestValues?.address, - // @ts-expect-error Property '_accounts' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._accounts, - wallets, - walletNames, - ]); - - useEffect(() => { - setCurrentNetwork( - ethereumUtils.getNetworkFromChainId( - Number( - transactionDetails?.walletConnectV2RequestValues?.chainId || - // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._chainId - ) - ) - ); - }, [ - transactionDetails?.walletConnectV2RequestValues?.chainId, - // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._chainId, - ]); + }, [wallets, currentAddress, walletNames]); useEffect(() => { const initProvider = async () => { let p; + // check on this o.O if (currentNetwork === Network.mainnet) { p = await getFlashbotsProvider(); } else { - p = await getProviderForNetwork(currentNetwork!); + p = await getProviderForNetwork(currentNetwork); } setProvider(p); }; - currentNetwork && initProvider(); + initProvider(); }, [currentNetwork, setProvider]); useEffect(() => { (async () => { - if (currentNetwork) { - const asset = await ethereumUtils.getNativeAssetForNetwork(currentNetwork!, accountInfo.address); - if (asset) { - provider && setNativeAsset(asset); - } + const asset = await ethereumUtils.getNativeAssetForNetwork(currentNetwork, accountInfo.address); + if (asset) { + provider && setNativeAsset(asset); } })(); }, [accountInfo.address, currentNetwork, provider]); useEffect(() => { (async () => { - if (accountInfo.address && currentNetwork && !isMessageRequest && !nonceForDisplay) { + if (!isMessageRequest && !nonceForDisplay) { try { - const nonce = await getNextNonce({ address: accountInfo.address, network: currentNetwork }); + console.log({ currentAddress, currentNetwork }); + const nonce = await getNextNonce({ address: currentAddress, network: currentNetwork }); if (nonce || nonce === 0) { const nonceAsString = nonce.toString(); setNonceForDisplay(nonceAsString); @@ -408,11 +378,7 @@ export const SignTransactionSheet = () => { useEffect(() => { const timeout = setTimeout(async () => { try { - const chainId = Number( - transactionDetails?.walletConnectV2RequestValues?.chainId || - // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._chainId - ); + const chainId = ethereumUtils.getChainIdFromNetwork(currentNetwork); let simulationData; if (isMessageRequest) { // Message Signing @@ -474,18 +440,7 @@ export const SignTransactionSheet = () => { return () => { clearTimeout(timeout); }; - }, [ - accountAddress, - currentNetwork, - isMessageRequest, - isPersonalSign, - req, - request.message, - simulationUnavailable, - transactionDetails, - // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) - walletConnector?._chainId, - ]); + }, [accountAddress, currentNetwork, isMessageRequest, isPersonalSign, req, request.message, simulationUnavailable, transactionDetails]); const closeScreen = useCallback( (canceled: boolean) => { @@ -499,58 +454,16 @@ export const SignTransactionSheet = () => { stopPollingGasFees(); } - let type: WalletconnectResultType = transactionDetails?.method === SEND_TRANSACTION ? 'transaction' : 'sign'; - if (canceled) { - type = `${type}-canceled`; - } - - if (pendingRedirect) { - InteractionManager.runAfterInteractions(() => { - dispatch(walletConnectRemovePendingRedirect(type, transactionDetails?.dappScheme)); - }); - } - - if (transactionDetails?.walletConnectV2RequestValues?.onComplete) { - InteractionManager.runAfterInteractions(() => { - transactionDetails?.walletConnectV2RequestValues.onComplete(type); - }); - } + onCloseScreenCallback?.(canceled); }, - [ - accountInfo.isHardwareWallet, - goBack, - isMessageRequest, - transactionDetails?.method, - transactionDetails?.walletConnectV2RequestValues, - transactionDetails?.dappScheme, - pendingRedirect, - stopPollingGasFees, - dispatch, - ] + [accountInfo.isHardwareWallet, goBack, isMessageRequest, onCloseScreenCallback, stopPollingGasFees] ); const onCancel = useCallback( async (error?: Error) => { try { - if (callback) { - callback({ error: error || 'User cancelled the request' }); - } setTimeout(async () => { - if (transactionDetails?.requestId) { - if (transactionDetails?.walletConnectV2RequestValues) { - await handleSessionRequestResponse(transactionDetails?.walletConnectV2RequestValues, { - result: null, - error: error || 'User cancelled the request', - }); - } else { - await dispatch( - walletConnectSendStatus(transactionDetails?.peerId, transactionDetails?.requestId, { - error: error || 'User cancelled the request', - }) - ); - } - dispatch(removeRequest(transactionDetails?.requestId)); - } + onCancelCallback?.(error); const rejectionType = transactionDetails?.payload?.method === SEND_TRANSACTION ? 'transaction' : 'signature'; analytics.track(`Rejected WalletConnect ${rejectionType} request`, { isHardwareWallet: accountInfo.isHardwareWallet, @@ -563,25 +476,13 @@ export const SignTransactionSheet = () => { closeScreen(true); } }, - [ - accountInfo.isHardwareWallet, - callback, - closeScreen, - dispatch, - transactionDetails?.payload?.method, - transactionDetails?.peerId, - transactionDetails?.requestId, - transactionDetails?.walletConnectV2RequestValues, - ] + [accountInfo.isHardwareWallet, closeScreen, onCancelCallback, transactionDetails?.payload?.method] ); const handleSignMessage = useCallback(async () => { const message = transactionDetails?.payload?.params.find((p: string) => !isAddress(p)); let response = null; - if (!currentNetwork) { - return; - } const provider = await getProviderForNetwork(currentNetwork); if (!provider) { return; @@ -610,20 +511,8 @@ export const SignTransactionSheet = () => { isHardwareWallet: accountInfo.isHardwareWallet, network: currentNetwork, }); - if (transactionDetails?.requestId) { - if (transactionDetails?.walletConnectV2RequestValues && response?.result) { - await handleSessionRequestResponse(transactionDetails?.walletConnectV2RequestValues, { - result: response.result, - error: null, - }); - } else { - await dispatch(walletConnectSendStatus(transactionDetails?.peerId, transactionDetails?.requestId, response)); - } - dispatch(removeRequest(transactionDetails?.requestId)); - } - if (callback) { - callback({ sig: response.result }); - } + onSuccessCallback?.(response.result); + closeScreen(false); } else { await onCancel(response?.error); @@ -633,15 +522,11 @@ export const SignTransactionSheet = () => { transactionDetails?.payload?.method, transactionDetails?.dappName, transactionDetails?.dappUrl, - transactionDetails?.requestId, - transactionDetails?.walletConnectV2RequestValues, - transactionDetails?.peerId, currentNetwork, accountInfo.address, accountInfo.isHardwareWallet, - callback, + onSuccessCallback, closeScreen, - dispatch, onCancel, ]); @@ -734,9 +619,6 @@ export const SignTransactionSheet = () => { if (response?.result) { const signResult = response.result as string; const sendResult = response.result as Transaction; - if (callback) { - callback({ result: sendInsteadOfSign ? sendResult.hash : signResult }); - } let txSavedInCurrentWallet = false; const displayDetails = transactionDetails.displayDetails; @@ -744,10 +626,10 @@ export const SignTransactionSheet = () => { if (sendInsteadOfSign && sendResult?.hash) { txDetails = { status: 'pending', - asset: nativeAsset || displayDetails?.request?.asset, + asset: displayDetails?.request?.asset || nativeAsset, contract: { - name: displayDetails.dappName, - iconUrl: displayDetails.dappIcon, + name: transactionDetails.dappName, + iconUrl: transactionDetails.imageUrl, }, data: sendResult.data, from: displayDetails?.request?.from, @@ -770,23 +652,18 @@ export const SignTransactionSheet = () => { } } analytics.track('Approved WalletConnect transaction request', { - dappName: displayDetails.dappName, - dappUrl: displayDetails.dappUrl, + dappName: transactionDetails.dappName, + dappUrl: transactionDetails.dappUrl, isHardwareWallet: accountInfo.isHardwareWallet, network: currentNetwork, }); - if (isFocused && transactionDetails?.requestId) { - if (transactionDetails?.walletConnectV2RequestValues && sendResult.hash) { - await handleSessionRequestResponse(transactionDetails?.walletConnectV2RequestValues, { - result: sendResult.hash, - error: null, - }); - } else { - if (sendResult.hash) { - await dispatch(walletConnectSendStatus(transactionDetails?.peerId, transactionDetails?.requestId, { result: sendResult.hash })); - } + + if (!sendInsteadOfSign) { + onSuccessCallback?.(signResult); + } else { + if (sendResult?.hash) { + onSuccessCallback?.(sendResult.hash); } - dispatch(removeRequest(transactionDetails?.requestId)); } closeScreen(false); @@ -806,7 +683,6 @@ export const SignTransactionSheet = () => { } else { logger.error(new RainbowError(`WC: Tx failure - ${formattedDappUrl}`), { dappName: transactionDetails?.dappName, - dappScheme: transactionDetails?.dappScheme, dappUrl: transactionDetails?.dappUrl, formattedDappUrl, rpcMethod: req?.method, @@ -820,25 +696,20 @@ export const SignTransactionSheet = () => { }, [ transactionDetails.payload.method, transactionDetails.displayDetails, - transactionDetails?.requestId, - transactionDetails?.walletConnectV2RequestValues, - transactionDetails?.peerId, - transactionDetails?.dappName, - transactionDetails?.dappScheme, - transactionDetails?.dappUrl, + transactionDetails.dappName, + transactionDetails.dappUrl, + transactionDetails.imageUrl, req, + currentNetwork, selectedGasFee, gasLimit, - provider, - currentNetwork, accountInfo.address, accountInfo.isHardwareWallet, - callback, - isFocused, + provider, closeScreen, nativeAsset, accountAddress, - dispatch, + onSuccessCallback, switchToWalletWithAddress, formattedDappUrl, onCancel, @@ -950,7 +821,7 @@ export const SignTransactionSheet = () => { { /> ) : ( { {`${walletBalance?.display} ${i18n.t(i18n.l.walletconnect.simulation.profile_section.on_network, { - network: getNetworkObj(currentNetwork!)?.name, + network: getNetworkObj(currentNetwork)?.name, })}`} @@ -1333,7 +1204,7 @@ const SimulationCard = ({ {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.need_more_native, { symbol: walletBalance?.symbol, - network: getNetworkObj(currentNetwork!).name, + network: getNetworkObj(currentNetwork).name, })} ) : ( diff --git a/src/screens/WalletConnectApprovalSheet.js b/src/screens/WalletConnectApprovalSheet.js index eeb5d4b8bd8..1ea2db23f37 100644 --- a/src/screens/WalletConnectApprovalSheet.js +++ b/src/screens/WalletConnectApprovalSheet.js @@ -292,7 +292,7 @@ export default function WalletConnectApprovalSheet() { }, watchOnly: true, }); - }, [approvalAccount.address, goBack, type, getState]); + }, [approvalAccount.address, goBack, type]); useEffect(() => { const waitingTime = (Date.now() - receivedTimestamp) / 1000; diff --git a/src/utils/requestNavigationHandlers.ts b/src/utils/requestNavigationHandlers.ts new file mode 100644 index 00000000000..7e8aaf4e3a1 --- /dev/null +++ b/src/utils/requestNavigationHandlers.ts @@ -0,0 +1,178 @@ +import { Navigation } from '@/navigation'; +import Routes from '@/navigation/routesNames'; + +// we should move these types since import from redux is not kosher +import { RequestData, WalletconnectRequestData, removeRequest } from '@/redux/requests'; +import store from '@/redux/store'; +import { + WalletconnectApprovalSheetRouteParams, + WalletconnectResultType, + walletConnectRemovePendingRedirect, + walletConnectSendStatus, +} from '@/redux/walletconnect'; +import { InteractionManager } from 'react-native'; +import { SEND_TRANSACTION } from './signingMethods'; +import { handleSessionRequestResponse } from '@/walletConnect'; +import ethereumUtils from './ethereumUtils'; +import { getRequestDisplayDetails } from '@/parsers'; +import { RainbowNetworks } from '@/networks'; +import { maybeSignUri } from '@/handlers/imgix'; +import { getActiveRoute } from '@/navigation/Navigation'; + +export type RequestType = 'walletconnect' | 'browser'; + +// Dapp Browser + +export interface DappConnectionData { + dappName: string; + dappUrl: string; + imageUrl: string | undefined; +} + +export const handleDappBrowserConnectionPrompt = (dappData: DappConnectionData): Promise<{ chainId: number; address: string } | Error> => { + return new Promise((resolve, reject) => { + const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); + const receivedTimestamp = Date.now(); + const routeParams: WalletconnectApprovalSheetRouteParams = { + receivedTimestamp, + meta: { + chainIds, + dappName: dappData.dappName, + dappUrl: dappData.dappUrl, + imageUrl: maybeSignUri(dappData.dappUrl), + isWalletConnectV2: false, + peerId: '', + dappScheme: null, + }, + timedOut: false, + callback: async (approved, approvedChainId, accountAddress) => { + if (approved) { + // if approved resolve with (chainId, address) + resolve({ chainId: approvedChainId, address: accountAddress }); + } else { + // else reject + reject(new Error('Connection not approved')); + } + }, + }; + + /** + * We might see this at any point in the app, so only use `replace` + * sometimes if the user is already looking at the approval sheet. + */ + Navigation.handleAction( + Routes.WALLET_CONNECT_APPROVAL_SHEET, + routeParams, + getActiveRoute()?.name === Routes.WALLET_CONNECT_APPROVAL_SHEET + ); + }); +}; + +export const handleDappBrowserRequest = async (request: Omit): Promise => { + const nativeCurrency = store.getState().settings.nativeCurrency; + const displayDetails = getRequestDisplayDetails(request.payload, nativeCurrency, request.network); + + const requestWithDetails: RequestData = { + ...request, + displayDetails, + }; + + return new Promise((resolve, reject) => { + const onSuccess = (result: string) => { + resolve(result); // Resolve the promise with the result string + }; + + const onCancel = (error?: Error) => { + if (error) { + reject(error); // Reject the promise with the provided error + } else { + reject(new Error('Operation cancelled by the user.')); // Reject with a default error if none provided + } + }; + + const onCloseScreen = (canceled: boolean) => { + // This function might not be necessary for the promise logic, + // but you can still use it for cleanup or logging if needed. + }; + + Navigation.handleAction(Routes.CONFIRM_REQUEST, { + transactionDetails: requestWithDetails, + onSuccess, + onCancel, + onCloseScreen, + network: request.network, + address: request.address, + requestType: 'browser', + }); + }); +}; + +// Walletconnect +export const handleWalletConnectRequest = async (request: WalletconnectRequestData) => { + const pendingRedirect = store.getState().walletconnect.pendingRedirect; + const walletConnector = store.getState().walletconnect.walletConnectors[request.peerId]; + + // @ts-expect-error Property '_chainId' is private and only accessible within class 'Connector'.ts(2341) + const network = ethereumUtils.getNetworkFromChainId(request?.walletConnectV2RequestValues?.chainId || walletConnector?._chainId); + // @ts-expect-error Property '_accounts' is private and only accessible within class 'Connector'.ts(2341) + const address = request?.walletConnectV2RequestValues?.address || walletConnector?._accounts?.[0]; + + const onSuccess = async (result: string) => { + if (request?.walletConnectV2RequestValues) { + await handleSessionRequestResponse(request?.walletConnectV2RequestValues, { + result: result, + error: null, + }); + } else { + await store.dispatch(walletConnectSendStatus(request?.peerId, request?.requestId, { result })); + } + store.dispatch(removeRequest(request?.requestId)); + }; + + const onCancel = async (error?: Error) => { + if (request?.requestId) { + if (request?.walletConnectV2RequestValues) { + await handleSessionRequestResponse(request?.walletConnectV2RequestValues, { + result: null, + error: error || 'User cancelled the request', + }); + } else { + await store.dispatch( + walletConnectSendStatus(request?.peerId, request?.requestId, { + error: error || 'User cancelled the request', + }) + ); + } + store.dispatch(removeRequest(request?.requestId)); + } + }; + + const onCloseScreen = (canceled: boolean) => { + let type: WalletconnectResultType = request.payload?.method === SEND_TRANSACTION ? 'transaction' : 'sign'; + console.log('type'); + if (canceled) { + type = `${type}-canceled`; + } + + if (pendingRedirect) { + InteractionManager.runAfterInteractions(() => { + store.dispatch(walletConnectRemovePendingRedirect(type, request?.dappScheme)); + }); + } + + if (request?.walletConnectV2RequestValues?.onComplete) { + InteractionManager.runAfterInteractions(() => { + request?.walletConnectV2RequestValues?.onComplete?.(type); + }); + } + }; + Navigation.handleAction(Routes.CONFIRM_REQUEST, { + transactionDetails: request, + onCancel, + onSuccess, + onCloseScreen, + network, + address, + requestType: 'walletconnect', + }); +}; diff --git a/src/walletConnect/index.tsx b/src/walletConnect/index.tsx index 64a0d44be02..b5ca6514206 100644 --- a/src/walletConnect/index.tsx +++ b/src/walletConnect/index.tsx @@ -24,9 +24,9 @@ import * as lang from '@/languages'; import store from '@/redux/store'; import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import WalletTypes from '@/helpers/walletTypes'; -import ethereumUtils from '@/utils/ethereumUtils'; +import ethereumUtils, { getNetworkFromChainId } from '@/utils/ethereumUtils'; import { getRequestDisplayDetails } from '@/parsers/requests'; -import { RequestData, REQUESTS_UPDATE_REQUESTS_TO_APPROVE, removeRequest } from '@/redux/requests'; +import { WalletconnectRequestData, REQUESTS_UPDATE_REQUESTS_TO_APPROVE, removeRequest } from '@/redux/requests'; import { saveLocalRequests } from '@/handlers/localstorage/walletconnectRequests'; import { events } from '@/handlers/appEvents'; import { getFCMToken } from '@/notifications/tokens'; @@ -42,6 +42,7 @@ import { RainbowNetworks } from '@/networks'; import { uniq } from 'lodash'; import { fetchDappMetadata } from '@/resources/metadata/dapp'; import { DAppStatus } from '@/graphql/__generated__/metadata'; +import { handleWalletConnectRequest } from '@/utils/requestNavigationHandlers'; const SUPPORTED_EVM_CHAIN_IDS = RainbowNetworks.filter(({ features }) => features.walletconnect).map(({ id }) => id); @@ -586,6 +587,11 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se method: method as RPCMethod, params, }); + if (!address) { + logger.error(new RainbowError('No Address in the RPC Params')); + return; + } + const allWallets = store.getState().wallets.wallets; logger.debug(`WC v2: session_request method is supported`, { method, params, address, message }, logger.DebugContext.walletconnect); @@ -670,7 +676,7 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se const dappNetwork = ethereumUtils.getNetworkFromChainId(chainId); const displayDetails = getRequestDisplayDetails(event.params.request, nativeCurrency, dappNetwork); const peerMeta = session.peer.metadata; - const request: RequestData = { + const request: WalletconnectRequestData = { clientId: session.topic, // I don't think this is used peerId: session.topic, // I don't think this is used requestId: event.id, @@ -679,12 +685,13 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se dappUrl: peerMeta.url || 'Unknown URL', displayDetails, imageUrl: maybeSignUri(peerMeta.icons[0], { w: 200 }), + address, + network: getNetworkFromChainId(chainId), payload: event.params.request, walletConnectV2RequestValues: { sessionRequestEvent: event, - // @ts-ignore we assign address above - address, // required by screen - chainId, // required by screen + address, + chainId, onComplete(type: string) { if (IS_IOS) { Navigation.handleAction(Routes.WALLET_CONNECT_REDIRECT_SHEET, { @@ -712,10 +719,7 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se logger.debug(`WC v2: navigating to CONFIRM_REQUEST sheet`, {}, logger.DebugContext.walletconnect); - Navigation.handleAction(Routes.CONFIRM_REQUEST, { - openAutomatically: true, - transactionDetails: request, - }); + handleWalletConnectRequest(request); analytics.track(analytics.event.wcShowingSigningRequest, { dappName: request.dappName,