diff --git a/app/actions/browser/index.js b/app/actions/browser/index.js index 64c3a2fd522..2d21c3a2182 100644 --- a/app/actions/browser/index.js +++ b/app/actions/browser/index.js @@ -2,18 +2,18 @@ * Browser actions for Redux */ export const BrowserActionTypes = { - ADD_TO_VISITED_DAPP: 'ADD_TO_VISITED_DAPP', + ADD_TO_VIEWED_DAPP: 'ADD_TO_VIEWED_DAPP', }; /** - * Adds a new entry to visited dapps + * Adds a new entry to viewed dapps * * @param {string} hostname - Dapp hostname * @returns */ -export function addToVisitedDapp(hostname) { +export function addToViewedDapp(hostname) { return { - type: BrowserActionTypes.ADD_TO_VISITED_DAPP, + type: BrowserActionTypes.ADD_TO_VIEWED_DAPP, hostname, }; } diff --git a/app/actions/sdk/index.ts b/app/actions/sdk/index.ts new file mode 100644 index 00000000000..9f0271208b3 --- /dev/null +++ b/app/actions/sdk/index.ts @@ -0,0 +1,194 @@ +import type { Action as ReduxAction } from 'redux'; +import { ConnectionProps } from '../../core/SDKConnect/Connection'; +import { ApprovedHosts, SDKSessions } from '../../core/SDKConnect/SDKConnect'; +import { WC2Metadata } from './state'; + +export enum ActionType { + WC2_METADATA = 'WC2_METADATA', + RESET_CONNECTIONS = 'RESET_CONNECTIONS', + UPDATE_CONNECTION = 'UPDATE_CONNECTION', + REMOVE_CONNECTION = 'REMOVE_CONNECTION', + ADD_CONNECTION = 'ADD_CONNECTION', + DISCONNECT_ALL = 'DISCONNECT_ALL', + REMOVE_APPROVED_HOST = 'REMOVE_APPROVWED_HOST', + SET_APPROVED_HOST = 'SET_APPROVED_HOST', + RESET_APPROVED_HOSTS = 'RESET_APPROVED_HOSTS', + SET_CONNECTED = 'SET_CONNECTED', + UPDATE_ANDROID_CONNECTION = 'UPDATE_ANDROID_CONNECTION', + REMOVE_ANDROID_CONNECTION = 'REMOVE_ANDROID_CONNECTION', + RESET_ANDROID_CONNECTIONS = 'RESET_ANDROID_CONNECTIONS', +} + +export type DisconnectAll = ReduxAction; + +export interface UpdateConnection + extends ReduxAction { + channelId: string; + connection: ConnectionProps; +} + +export interface ResetConnection + extends ReduxAction { + connections: SDKSessions; +} + +export interface RemoveConnection + extends ReduxAction { + channelId: string; +} + +export interface AddConnection extends ReduxAction { + channelId: string; + connection: ConnectionProps; +} + +export interface RemoveApprovedHost + extends ReduxAction { + channelId: string; +} + +export interface SetApprovedHost + extends ReduxAction { + channelId: string; + validUntil: number; +} + +export interface ResetApprovedHosts + extends ReduxAction { + approvedHosts: ApprovedHosts; +} + +export interface UpdateAndroidConnection + extends ReduxAction { + channelId: string; + connection: ConnectionProps; +} + +export interface RemoveAndroidConnection + extends ReduxAction { + channelId: string; +} + +export interface ResetAndroidConnections + extends ReduxAction { + connections: SDKSessions; +} + +export interface SetConnected extends ReduxAction { + channelId: string; + connected: boolean; +} + +export interface UpdateWC2Metadata + extends ReduxAction { + metadata?: WC2Metadata; +} + +export type Action = + | UpdateConnection + | DisconnectAll + | RemoveConnection + | AddConnection + | ResetConnection + | RemoveApprovedHost + | SetApprovedHost + | ResetApprovedHosts + | UpdateWC2Metadata + | UpdateAndroidConnection + | RemoveAndroidConnection + | ResetAndroidConnections + | SetConnected; + +export const disconnectAll = (): DisconnectAll => ({ + type: ActionType.DISCONNECT_ALL, +}); + +export const updateWC2Metadata = ( + metadata: WC2Metadata, +): UpdateWC2Metadata => ({ + type: ActionType.WC2_METADATA, + metadata, +}); + +export const updateConnection = ( + channelId: string, + connection: ConnectionProps, +): UpdateConnection => ({ + type: ActionType.UPDATE_CONNECTION, + channelId, + connection, +}); + +export const removeConnection = (channelId: string): RemoveConnection => ({ + type: ActionType.REMOVE_CONNECTION, + channelId, +}); + +export const addConnection = ( + channelId: string, + connection: ConnectionProps, +): AddConnection => ({ + type: ActionType.ADD_CONNECTION, + channelId, + connection, +}); + +export const resetConnections = ( + connections: SDKSessions, +): ResetConnection => ({ + type: ActionType.RESET_CONNECTIONS, + connections, +}); + +export const removeApprovedHost = (channelId: string): RemoveApprovedHost => ({ + type: ActionType.REMOVE_APPROVED_HOST, + channelId, +}); + +export const setApprovedHost = ( + channelId: string, + validUntil: number, +): SetApprovedHost => ({ + type: ActionType.SET_APPROVED_HOST, + channelId, + validUntil, +}); + +export const resetApprovedHosts = ( + approvedHosts: ApprovedHosts, +): ResetApprovedHosts => ({ + type: ActionType.RESET_APPROVED_HOSTS, + approvedHosts, +}); + +export const updateAndroidConnection = ( + channelId: string, + connection: ConnectionProps, +): UpdateAndroidConnection => ({ + type: ActionType.UPDATE_ANDROID_CONNECTION, + channelId, + connection, +}); + +export const removeAndroidConnection = ( + channelId: string, +): RemoveAndroidConnection => ({ + type: ActionType.REMOVE_ANDROID_CONNECTION, + channelId, +}); + +export const resetAndroidConnections = ( + connections: SDKSessions, +): ResetAndroidConnections => ({ + type: ActionType.RESET_ANDROID_CONNECTIONS, + connections, +}); + +export const setConnected = ( + channelId: string, + connected: boolean, +): SetConnected => ({ + type: ActionType.SET_CONNECTED, + channelId, + connected, +}); diff --git a/app/actions/sdk/state.ts b/app/actions/sdk/state.ts new file mode 100644 index 00000000000..c231e7dfdef --- /dev/null +++ b/app/actions/sdk/state.ts @@ -0,0 +1,14 @@ +import { ApprovedHosts, SDKSessions } from '../../core/SDKConnect/SDKConnect'; +export interface WC2Metadata { + id: string; + url: string; + name: string; + icon: string; +} +export interface SDKState { + connections: SDKSessions; + approvedHosts: ApprovedHosts; + androidConnections: SDKSessions; + // Link to metadata of last created wallet connect session. + wc2Metadata?: WC2Metadata; +} diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx index 305d3e9d03a..8ff8fbedd78 100644 --- a/app/components/Views/AccountConnect/AccountConnect.tsx +++ b/app/components/Views/AccountConnect/AccountConnect.tsx @@ -55,8 +55,9 @@ import useFavicon from '../../hooks/useFavicon/useFavicon'; import URLParse from 'url-parse'; import SDKConnect from '../../../core/SDKConnect/SDKConnect'; import AppConstants from '../../../../app/core/AppConstants'; -import { trackDappVisitedEvent } from '../../../util/metrics'; +import { trackDappViewedEvent } from '../../../util/metrics'; import { useMetrics } from '../../../components/hooks/useMetrics'; +import { RootState } from '../../../../app/reducers'; const AccountConnect = (props: AccountConnectProps) => { const Engine = UntypedEngine as any; @@ -92,28 +93,46 @@ const AccountConnect = (props: AccountConnectProps) => { origin: string; }; - // Extract connection info from sdk - // FIXME should be replaced by passing dynamic parameters to the PermissionController - // TODO: Retrive wallet connect connection info from channelId - const sdkConnection = SDKConnect.getInstance().getConnection({ channelId }); - const hostname = ( - sdkConnection?.originatorInfo?.url ?? metadataOrigin - ).replace(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN, ''); - const origin: string = useSelector(getActiveTabUrl, isEqual); + const accountsLength = useSelector(selectAccountsLength); - const faviconSource = useFavicon(origin); + const [hostname, setHostname] = useState(origin); const urlWithProtocol = prefixUrlWithProtocol(hostname); + const sdkConnection = SDKConnect.getInstance().getConnection({ channelId }); + // Last wallet connect session metadata + const wc2Metadata = useSelector((state: RootState) => state.sdk.wc2Metadata); + + const dappIconUrl = sdkConnection.originatorInfo?.icon; + const faviconSource = useFavicon(origin); + + const actualIcon = useMemo( + () => (dappIconUrl ? { uri: dappIconUrl } : faviconSource), + [dappIconUrl, faviconSource], + ); const secureIcon = useMemo( () => - (getUrlObj(origin) as URLParse).protocol === 'https:' + (getUrlObj(hostname) as URLParse).protocol === 'https:' ? IconName.Lock : IconName.LockSlash, - [origin], + [hostname], ); - const accountsLength = useSelector(selectAccountsLength); + const loadHostname = useCallback(async () => { + if (sdkConnection) { + const _hostname = ( + sdkConnection?.originatorInfo?.url ?? metadataOrigin + ).replace(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN, ''); + return _hostname; + } + + return wc2Metadata?.url ?? channelId; + }, [channelId, metadataOrigin, sdkConnection, wc2Metadata]); + + // Retrieve hostname info based on channelId + useEffect(() => { + loadHostname().then(setHostname); + }, [hostname, setHostname, loadHostname]); // Refreshes selected addresses based on the addition and removal of accounts. useEffect(() => { @@ -152,10 +171,10 @@ const AccountConnect = (props: AccountConnectProps) => { ], ); - const triggerDappVisitedEvent = useCallback( + const triggerDappViewedEvent = useCallback( (numberOfConnectedAccounts: number) => - // Track dapp visited event - trackDappVisitedEvent({ hostname, numberOfConnectedAccounts }), + // Track dapp viewed event + trackDappViewedEvent({ hostname, numberOfConnectedAccounts }), [hostname], ); @@ -186,7 +205,7 @@ const AccountConnect = (props: AccountConnectProps) => { request, ); - triggerDappVisitedEvent(connectedAccountLength); + triggerDappViewedEvent(connectedAccountLength); trackEvent(MetaMetricsEvents.CONNECT_REQUEST_COMPLETED, { number_of_accounts: accountsLength, @@ -231,7 +250,7 @@ const AccountConnect = (props: AccountConnectProps) => { toastRef, accountsLength, metadataOrigin, - triggerDappVisitedEvent, + triggerDappViewedEvent, trackEvent, ]); @@ -350,7 +369,7 @@ const AccountConnect = (props: AccountConnectProps) => { onUserAction={setUserIntent} defaultSelectedAccount={defaultSelectedAccount} isLoading={isLoading} - favicon={faviconSource} + favicon={actualIcon} secureIcon={secureIcon} urlWithProtocol={urlWithProtocol} /> @@ -362,11 +381,11 @@ const AccountConnect = (props: AccountConnectProps) => { isLoading, setScreen, setSelectedAddresses, - faviconSource, + actualIcon, secureIcon, + sdkConnection, urlWithProtocol, setUserIntent, - sdkConnection, ]); const renderSingleConnectSelectorScreen = useCallback( diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index 539a36ccdae..61fc1c71bbc 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -99,7 +99,7 @@ import { regex } from '../../../../app/util/regex'; import { selectChainId } from '../../../selectors/networkController'; import { BrowserViewSelectorsIDs } from '../../../../e2e/selectors/BrowserView.selectors'; import { useMetrics } from '../../../components/hooks/useMetrics'; -import trackDappVisitedEvent from '../../../util/metrics/trackDappVisited'; +import { trackDappViewedEvent } from '../../../util/metrics'; import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; const { HOMEPAGE_URL, NOTIFICATION_NAMES } = AppConstants; @@ -531,7 +531,7 @@ export const BrowserTab = (props) => { [goBack, props.ipfsGateway, setIpfsBannerVisible, props.chainId], ); - const triggerDappVisitedEvent = (url) => { + const triggerDappViewedEvent = (url) => { const permissionsControllerState = Engine.context.PermissionController.state; const hostname = new URL(url).hostname; @@ -545,8 +545,8 @@ export const BrowserTab = (props) => { return; } - // Track dapp visited event - trackDappVisitedEvent({ + // Track dapp viewed event + trackDappViewedEvent({ hostname, numberOfConnectedAccounts: connectedAccounts.length, }); @@ -596,7 +596,7 @@ export const BrowserTab = (props) => { // Skip tracking on initial open if (!initialCall) { - triggerDappVisitedEvent(urlToGo); + triggerDappViewedEvent(urlToGo); } setProgress(0); @@ -627,7 +627,7 @@ export const BrowserTab = (props) => { const { current } = webviewRef; current && current.reload(); - triggerDappVisitedEvent(url.current); + triggerDappViewedEvent(url.current); }, []); /** diff --git a/app/components/Views/SDKDisconnectModal/SDKDisconnectModal.tsx b/app/components/Views/SDKDisconnectModal/SDKDisconnectModal.tsx index 994ae38f579..85f9a755ee7 100644 --- a/app/components/Views/SDKDisconnectModal/SDKDisconnectModal.tsx +++ b/app/components/Views/SDKDisconnectModal/SDKDisconnectModal.tsx @@ -2,8 +2,8 @@ import React, { useMemo, useRef } from 'react'; // External dependencies -import { ThemeColors } from '@metamask/design-tokens/dist/js/themes/types'; -import { ThemeTypography } from '@metamask/design-tokens/dist/js/typography'; +import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types'; +import type { ThemeTypography } from '@metamask/design-tokens/dist/types/js/typography'; import { useNavigation } from '@react-navigation/native'; import { StyleSheet, View } from 'react-native'; import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context'; diff --git a/app/components/Views/SDKSessionsManager/SDKSessionAccountListItem.tsx b/app/components/Views/SDKSessionsManager/SDKSessionAccountListItem.tsx index abb005618a7..679fd20c643 100644 --- a/app/components/Views/SDKSessionsManager/SDKSessionAccountListItem.tsx +++ b/app/components/Views/SDKSessionsManager/SDKSessionAccountListItem.tsx @@ -1,5 +1,5 @@ -import { ThemeColors } from '@metamask/design-tokens/dist/js/themes/types'; -import { ThemeTypography } from '@metamask/design-tokens/dist/js/typography'; +import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types'; +import type { ThemeTypography } from '@metamask/design-tokens/dist/types/js/typography'; import React, { useEffect, useState } from 'react'; import { StyleSheet, TextStyle, View } from 'react-native'; import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context'; diff --git a/app/components/Views/SDKSessionsManager/SDKSessionItem.tsx b/app/components/Views/SDKSessionsManager/SDKSessionItem.tsx index 0f865f02afb..f7e0a9ebb8c 100644 --- a/app/components/Views/SDKSessionsManager/SDKSessionItem.tsx +++ b/app/components/Views/SDKSessionsManager/SDKSessionItem.tsx @@ -1,5 +1,5 @@ -import { ThemeColors } from '@metamask/design-tokens/dist/js/themes/types'; -import { ThemeTypography } from '@metamask/design-tokens/dist/js/typography'; +import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types'; +import type { ThemeTypography } from '@metamask/design-tokens/dist/types/js/typography'; import { useNavigation } from '@react-navigation/native'; import React, { useCallback, useEffect, useState } from 'react'; import { StyleSheet, TextStyle, View } from 'react-native'; @@ -30,9 +30,9 @@ interface SDKSessionViewProps { connection: { id: ConnectionProps['id']; originatorInfo?: ConnectionProps['originatorInfo']; + connected?: ConnectionProps['connected']; }; trigger?: number; // used to force refresh fetching permitted accounts - connected?: boolean; } const createStyles = ( @@ -85,7 +85,6 @@ const createStyles = ( export const SDKSessionItem = ({ connection, - connected = false, trigger, }: SDKSessionViewProps) => { const safeAreaInsets = useSafeAreaInsets(); @@ -99,7 +98,7 @@ export const SDKSessionItem = ({ >([]); DevLogger.log( - `Rendering SDKSessionItem connected=${connected} ${connection.id}`, + `Rendering SDKSessionItem connected=${connection.connected} ${connection.id}`, ); useEffect(() => { let _sessionName = connection.id; @@ -154,7 +153,7 @@ export const SDKSessionItem = ({ { const route = useRoute>(); - const sdk = SDKConnect.getInstance(); + const { connections, androidConnections } = useSelector( + (state: RootState) => state.sdk, + ); + const connectionsList = Object.values(connections); + const androidConnectionsList = Object.values(androidConnections); const { trigger } = route.params ?? { trigger: undefined }; const { colors, typography } = useTheme(); const styles = createStyles(colors, typography, safeAreaInsets); - const [connections, setConnections] = useState([]); - const [connectedIds, setConnectedIds] = useState([]); - const [androidConnections, setAndroidConnections] = useState( - [], - ); + const { navigate } = useNavigation(); const toggleClearMMSDKConnectionModal = useCallback(() => { @@ -88,34 +87,6 @@ const SDKSessionsManager = (props: SDKSessionsManagerProps) => { }, [navigate]); useEffect(() => { - const refreshSDKState = async () => { - const _connections = sdk.getConnections(); - const _connected = sdk.getConnected(); - setConnectedIds( - Object.values(_connected) - .filter((connectionSource) => connectionSource.isReady) - .map((connectionSource) => connectionSource.channelId), - ); - - const connectionsList = Object.values(_connections); - - try { - const _androidConnections = sdk.getAndroidConnections() ?? []; - setAndroidConnections(_androidConnections); - } catch (error) { - console.error('Failed to load Android connections:', error); - } - // Sort connection by validity - connectionsList.sort((a, b) => { - // Provide a fallback value (e.g., 0) if 'validUntil' is undefined - const aValue = a.validUntil ?? 0; - const bValue = b.validUntil ?? 0; - - return bValue - aValue; - }); - setConnections(connectionsList); - }; - const { navigation } = props; navigation.setOptions( getNavigationOptionsTitle( @@ -125,38 +96,23 @@ const SDKSessionsManager = (props: SDKSessionsManagerProps) => { colors, ), ); - sdk.on('refresh', () => { - refreshSDKState(); - }); - refreshSDKState(); - - return () => { - sdk.off('refresh', () => { - refreshSDKState(); - }); - }; - }, [sdk, colors, props]); + }, [props, colors]); const renderSDKSessions = useCallback( () => ( <> - {connections.map((sdkSession, _index) => ( + {connectionsList.map((sdkSession, _index) => ( ))} - {androidConnections.map((androidSession, _index) => ( + {androidConnectionsList.map((androidSession, _index) => ( ))} @@ -173,10 +129,9 @@ const SDKSessionsManager = (props: SDKSessionsManagerProps) => { ), [ - connectedIds, + connectionsList, trigger, - connections, - androidConnections, + androidConnectionsList, styles, toggleClearMMSDKConnectionModal, ], @@ -199,7 +154,7 @@ const SDKSessionsManager = (props: SDKSessionsManagerProps) => { style={styles.wrapper} testID={SDKSelectorsIDs.SESSION_MANAGER_CONTAINER} > - {connections.length + androidConnections.length > 0 + {connectionsList.length + androidConnectionsList.length > 0 ? renderSDKSessions() : renderEmptyResult()} diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index aad8067e9bd..9c8a8ece16a 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -151,7 +151,7 @@ enum EVENT_NAME { BROWSER_RELOAD = 'Reload Browser', BROWSER_ADD_FAVORITES = 'Added Site To Favorites', BROWSER_SWITCH_TAB = 'Switched tab within Browser', - DAPP_VISITED = 'Dapp Visited', + DAPP_VIEWED = 'Dapp Viewed', // Security & Privacy Settings VIEW_SECURITY_SETTINGS = 'Views Security & Privacy', @@ -534,7 +534,7 @@ const events = { BROWSER_SHARE_SITE: generateOpt(EVENT_NAME.BROWSER_SHARE_SITE), BROWSER_RELOAD: generateOpt(EVENT_NAME.BROWSER_RELOAD), BROWSER_ADD_FAVORITES: generateOpt(EVENT_NAME.BROWSER_ADD_FAVORITES), - DAPP_VISITED: generateOpt(EVENT_NAME.DAPP_VISITED), + DAPP_VIEWED: generateOpt(EVENT_NAME.DAPP_VIEWED), // Security & Privacy Settings VIEW_SECURITY_SETTINGS: generateOpt(EVENT_NAME.VIEW_SECURITY_SETTINGS), // Reveal SRP diff --git a/app/core/BackgroundBridge/BackgroundBridge.js b/app/core/BackgroundBridge/BackgroundBridge.js index 7f561bc29ef..7d090694182 100644 --- a/app/core/BackgroundBridge/BackgroundBridge.js +++ b/app/core/BackgroundBridge/BackgroundBridge.js @@ -223,18 +223,6 @@ export class BackgroundBridge extends EventEmitter { async notifySelectedAddressChanged(selectedAddress) { try { - // Remove following specific walletconnect block once WalletConnect has migrated away from DefaultPreferences - if (this.isWalletConnect) { - DevLogger.log( - `notifySelectedAddressChanged walletconnect hostname: ${this.hostname}: ${selectedAddress}`, - ); - this.sendNotification({ - method: NOTIFICATION_NAMES.accountsChanged, - params: [selectedAddress], - }); - return; - } - let approvedAccounts = await getPermittedAccounts( this.channelId ?? this.hostname, ); diff --git a/app/core/RPCMethods/RPCMethodMiddleware.test.ts b/app/core/RPCMethods/RPCMethodMiddleware.test.ts index 62c4311c3a9..dd4ecfc8363 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.test.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.test.ts @@ -657,6 +657,7 @@ describe('getRpcMethodMiddleware', () => { setupGlobalState({ addTransactionResult: Promise.resolve('fake-hash'), // Set minimal network controller state to support validation + permittedAccounts: { 'example.metamask.io': [] }, providerConfig: { chainId: '0x1', type: RPC, diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 633a69e5d4f..cce84224203 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -98,13 +98,11 @@ export const checkActiveAccountAndChainId = async ({ address, chainId, channelId, - checkSelectedAddress, hostname, }: { address?: string; chainId?: number; channelId?: string; - checkSelectedAddress: boolean; hostname: string; }) => { let isInvalidAccount = false; @@ -114,7 +112,6 @@ export const checkActiveAccountAndChainId = async ({ address, chainId, channelId, - checkSelectedAddress, hostname, formattedAddress, }); @@ -123,24 +120,17 @@ export const checkActiveAccountAndChainId = async ({ '', ); - if (checkSelectedAddress) { - const selectedAddress = - Engine.context.PreferencesController.state.selectedAddress; - if (formattedAddress !== safeToChecksumAddress(selectedAddress)) { - isInvalidAccount = true; - } - } else { - const accounts = await getPermittedAccounts(channelId ?? validHostname); - const normalizedAccounts = accounts.map(safeToChecksumAddress); - - if (!normalizedAccounts.includes(formattedAddress)) { - isInvalidAccount = true; - if (accounts.length > 0) { - // Permissions issue --- requesting incorrect address - throw ethErrors.rpc.invalidParams({ - message: `Invalid parameters: must provide a permitted Ethereum address.`, - }); - } + const accounts = + (await getPermittedAccounts(channelId ?? validHostname)) ?? []; + const normalizedAccounts = accounts.map(safeToChecksumAddress); + + if (!normalizedAccounts.includes(formattedAddress)) { + isInvalidAccount = true; + if (accounts.length > 0) { + // Permissions issue --- requesting incorrect address + throw ethErrors.rpc.invalidParams({ + message: `Invalid parameters: must provide a permitted Ethereum address.`, + }); } } @@ -194,7 +184,6 @@ const generateRawSignature = async ({ title, icon, analytics, - isWalletConnect, chainId, channelId, getSource, @@ -220,7 +209,6 @@ const generateRawSignature = async ({ channelId, address: req.params[0], chainId, - checkSelectedAddress: isWalletConnect, }); const rawSig = await SignatureController.newUnsignedTypedMessage( @@ -512,7 +500,6 @@ export const getRpcMethodMiddleware = ({ address: from, channelId, chainId, - checkSelectedAddress: isWalletConnect, }); }, }); @@ -551,7 +538,6 @@ export const getRpcMethodMiddleware = ({ hostname, channelId, address: req.params[0].from, - checkSelectedAddress: isWalletConnect, }); PPOMUtil.validateRequest(req); const rawSig = await SignatureController.newUnsignedMessage({ @@ -599,7 +585,6 @@ export const getRpcMethodMiddleware = ({ hostname, channelId, address: params.from, - checkSelectedAddress: isWalletConnect, }); PPOMUtil.validateRequest(req); @@ -647,7 +632,6 @@ export const getRpcMethodMiddleware = ({ hostname, channelId, address: req.params[1], - checkSelectedAddress: isWalletConnect, }); PPOMUtil.validateRequest(req); diff --git a/app/core/SDKConnect/AndroidSDK/AndroidService.ts b/app/core/SDKConnect/AndroidSDK/AndroidService.ts index b8e2aaea857..75c978e8e84 100644 --- a/app/core/SDKConnect/AndroidSDK/AndroidService.ts +++ b/app/core/SDKConnect/AndroidSDK/AndroidService.ts @@ -40,9 +40,12 @@ import DevLogger from '../utils/DevLogger'; import AndroidSDKEventHandler from './AndroidNativeSDKEventHandler'; import { AndroidClient } from './android-sdk-types'; +export interface AndroidConnections { + [clientId: string]: AndroidClient; +} export default class AndroidService extends EventEmitter2 { private communicationClient = NativeModules.CommunicationClient; - private connections: { [clientId: string]: AndroidClient } = {}; + private connections: AndroidConnections = {}; private rpcQueueManager = new RPCQueueManager(); private bridgeByClientId: { [clientId: string]: BackgroundBridge } = {}; private eventHandler: AndroidSDKEventHandler; diff --git a/app/core/SDKConnect/AndroidSDK/addAndroidConnection.test.ts b/app/core/SDKConnect/AndroidSDK/addAndroidConnection.test.ts index 18b4cce4d3d..a57e21f4d30 100644 --- a/app/core/SDKConnect/AndroidSDK/addAndroidConnection.test.ts +++ b/app/core/SDKConnect/AndroidSDK/addAndroidConnection.test.ts @@ -1,5 +1,3 @@ -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../../core/AppConstants'; import { ConnectionProps } from '../Connection'; import SDKConnect from '../SDKConnect'; import addAndroidConnection from './addAndroidConnection'; @@ -39,28 +37,4 @@ describe('addAndroidConnection', () => { mockConnection, ); }); - - it('should save the updated connections to DefaultPreference', async () => { - const mockConnection = { - id: 'test-id', - } as unknown as ConnectionProps; - - await addAndroidConnection(mockConnection, mockInstance); - - expect(DefaultPreference.set).toHaveBeenCalledWith( - AppConstants.MM_SDK.ANDROID_CONNECTIONS, - JSON.stringify(mockInstance.state.androidConnections), - ); - }); - - it('should emit a refresh event', async () => { - const mockConnection = { - id: 'test-id', - } as unknown as ConnectionProps; - - await addAndroidConnection(mockConnection, mockInstance); - - expect(mockEmit).toHaveBeenCalledTimes(1); - expect(mockEmit).toHaveBeenCalledWith('refresh'); - }); }); diff --git a/app/core/SDKConnect/AndroidSDK/addAndroidConnection.ts b/app/core/SDKConnect/AndroidSDK/addAndroidConnection.ts index 13c97c14e00..b298ec122da 100644 --- a/app/core/SDKConnect/AndroidSDK/addAndroidConnection.ts +++ b/app/core/SDKConnect/AndroidSDK/addAndroidConnection.ts @@ -1,8 +1,8 @@ +import { updateAndroidConnection } from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import { ConnectionProps } from '../Connection'; import SDKConnect from '../SDKConnect'; import DevLogger from '../utils/DevLogger'; -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../../core/AppConstants'; async function addAndroidConnection( connection: ConnectionProps, @@ -12,14 +12,7 @@ async function addAndroidConnection( DevLogger.log(`SDKConnect::addAndroidConnection`, connection); - await DefaultPreference.set( - AppConstants.MM_SDK.ANDROID_CONNECTIONS, - JSON.stringify(instance.state.androidConnections), - ).catch((err) => { - throw err; - }); - - instance.emit('refresh'); + store.dispatch(updateAndroidConnection(connection.id, connection)); } export default addAndroidConnection; diff --git a/app/core/SDKConnect/AndroidSDK/loadAndroidConnections.test.ts b/app/core/SDKConnect/AndroidSDK/loadAndroidConnections.test.ts index 011d4432146..1467bb71087 100644 --- a/app/core/SDKConnect/AndroidSDK/loadAndroidConnections.test.ts +++ b/app/core/SDKConnect/AndroidSDK/loadAndroidConnections.test.ts @@ -1,5 +1,4 @@ import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../../core/AppConstants'; import loadAndroidConnections from './loadAndroidConnections'; jest.mock('../../../core/AppConstants'); @@ -14,14 +13,6 @@ describe('loadAndroidConnections', () => { jest.clearAllMocks(); }); - it('should retrieve Android connections from DefaultPreference', async () => { - await loadAndroidConnections(); - - expect(DefaultPreference.get).toHaveBeenCalledWith( - AppConstants.MM_SDK.ANDROID_CONNECTIONS, - ); - }); - it('should return an empty object if no connections are found', async () => { const result = await loadAndroidConnections(); @@ -29,11 +20,7 @@ describe('loadAndroidConnections', () => { }); it('should parse the retrieved connections', async () => { - const mockConnections = { - 'test-id': { - id: 'test-id', - }, - }; + const mockConnections = {}; (DefaultPreference.get as jest.Mock).mockResolvedValueOnce( JSON.stringify(mockConnections), diff --git a/app/core/SDKConnect/AndroidSDK/loadAndroidConnections.ts b/app/core/SDKConnect/AndroidSDK/loadAndroidConnections.ts index 89782865b18..2f61a3468ed 100644 --- a/app/core/SDKConnect/AndroidSDK/loadAndroidConnections.ts +++ b/app/core/SDKConnect/AndroidSDK/loadAndroidConnections.ts @@ -1,23 +1,21 @@ -import AppConstants from '../../../core/AppConstants'; +import { RootState } from '../../../../app/reducers'; +import { store } from '../../../../app/store'; import { ConnectionProps } from '../Connection'; -import DefaultPreference from 'react-native-default-preference'; import DevLogger from '../utils/DevLogger'; async function loadAndroidConnections(): Promise<{ [id: string]: ConnectionProps; }> { - const rawConnections = await DefaultPreference.get( - AppConstants.MM_SDK.ANDROID_CONNECTIONS, - ); - - if (!rawConnections) return {}; + const { sdk } = store.getState() as RootState; - const parsed = JSON.parse(rawConnections); + const androidConnections = sdk.androidConnections || {}; DevLogger.log( - `SDKConnect::loadAndroidConnections found ${Object.keys(parsed).length}`, - parsed, + `SDKConnect::loadAndroidConnections found ${ + Object.keys(androidConnections).length + }`, + androidConnections, ); - return parsed; + return androidConnections; } export default loadAndroidConnections; diff --git a/app/core/SDKConnect/Connection/Connection.ts b/app/core/SDKConnect/Connection/Connection.ts index d3e291ae739..aac1fb203ab 100644 --- a/app/core/SDKConnect/Connection/Connection.ts +++ b/app/core/SDKConnect/Connection/Connection.ts @@ -39,6 +39,7 @@ export interface ConnectionProps { initialConnection?: boolean; navigation?: NavigationContainerRef; originatorInfo?: OriginatorInfo; + connected?: boolean; validUntil?: number; lastAuthorized?: number; // timestamp of last received activity } diff --git a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsDisconnected.test.ts b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsDisconnected.test.ts index 81567ebb577..e7302b827cb 100644 --- a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsDisconnected.test.ts +++ b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsDisconnected.test.ts @@ -72,27 +72,6 @@ describe('handleClientsDisconnected', () => { expect(mockConnection.otps).toBeUndefined(); }); - describe('When clients ready event has not been received', () => { - beforeEach(() => { - mockConnection.receivedClientsReady = false; - }); - - it('should disconnect with termination and specific context', () => { - const handler = handleClientsDisconnected({ - instance: mockConnection, - disapprove: mockDisapprove, - }); - - handler(); - - expect(mockDisconnect).toHaveBeenCalledTimes(1); - expect(mockDisconnect).toHaveBeenCalledWith({ - terminate: true, - context: 'CLIENTS_DISCONNECTED', - }); - }); - }); - it('should set receivedDisconnect to true', () => { const handler = handleClientsDisconnected({ instance: mockConnection, diff --git a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsDisconnected.ts b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsDisconnected.ts index fc1ed307f57..d66bcbce73d 100644 --- a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsDisconnected.ts +++ b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsDisconnected.ts @@ -30,12 +30,13 @@ function handleClientsDisconnected({ // detect interruption of connection (can happen on mobile browser ios) - We need to warm the user to redo the connection. if (!instance.receivedClientsReady && !instance.remote.isPaused()) { - // SOCKET CONNECTION WAS INTERRUPTED - console.warn( - `Connected::clients_disconnected dApp connection disconnected before ready`, - ); - // Terminate to prevent bypassing initial approval when auto-reconnect on deeplink. - instance.disconnect({ terminate: true, context: 'CLIENTS_DISCONNECTED' }); + // Only disconnect on deeplinks + if (instance.origin === AppConstants.DEEPLINKS.ORIGIN_DEEPLINK) { + // SOCKET CONNECTION WAS INTERRUPTED + console.warn( + `Connected::clients_disconnected dApp connection disconnected before ready`, + ); + } } instance.receivedDisconnect = true; diff --git a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts index c2d6a5a08b5..c222d022571 100644 --- a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts +++ b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts @@ -38,7 +38,6 @@ function handleClientsReady({ // Remove connection from SDK completely SDKConnect.getInstance().removeChannel({ channelId: instance.channelId, - emitRefresh: true, sendTerminate: true, }); diff --git a/app/core/SDKConnect/ConnectionManagement/approveHost.test.ts b/app/core/SDKConnect/ConnectionManagement/approveHost.test.ts index 1f0175303c8..4a95b6f16b2 100644 --- a/app/core/SDKConnect/ConnectionManagement/approveHost.test.ts +++ b/app/core/SDKConnect/ConnectionManagement/approveHost.test.ts @@ -1,9 +1,8 @@ -import DefaultPreference from 'react-native-default-preference'; import AppConstants from '../../../core/AppConstants'; import SDKConnect from '../SDKConnect'; +import { DEFAULT_SESSION_TIMEOUT_MS } from '../SDKConnectConstants'; import DevLogger from '../utils/DevLogger'; import approveHost from './approveHost'; -import { DEFAULT_SESSION_TIMEOUT_MS } from '../SDKConnectConstants'; jest.mock('../../../core/AppConstants'); jest.mock('../SDKConnect'); @@ -49,10 +48,6 @@ describe('approveHost', () => { `SDKConnect approveHost ${mockHost}`, mockInstance.state.approvedHosts, ); - expect(DefaultPreference.set).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - 'mockStringifiedApprovedHosts', - ); }); it('should update the approved hosts list in the instance state', () => { @@ -142,28 +137,6 @@ describe('approveHost', () => { host: mockHost, instance: mockInstance, }); - - expect(DefaultPreference.set).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - 'mockStringifiedApprovedHosts', - ); - }); - - it('should emit a refresh event', () => { - const mockHost = 'mockHost'; - const mockApprovedUntil = 1234567890; - - jest.spyOn(Date, 'now').mockReturnValueOnce(mockApprovedUntil); - jest - .spyOn(JSON, 'stringify') - .mockReturnValueOnce('mockStringifiedApprovedHosts'); - - approveHost({ - host: mockHost, - instance: mockInstance, - }); - - expect(mockInstance.emit).toHaveBeenCalledWith('refresh'); }); describe('Handling disabled hosts', () => { diff --git a/app/core/SDKConnect/ConnectionManagement/approveHost.ts b/app/core/SDKConnect/ConnectionManagement/approveHost.ts index 6ae2d27497b..b98b1cd38f1 100644 --- a/app/core/SDKConnect/ConnectionManagement/approveHost.ts +++ b/app/core/SDKConnect/ConnectionManagement/approveHost.ts @@ -1,8 +1,12 @@ +import { + resetApprovedHosts, + resetConnections, +} from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import AppConstants from '../../../core/AppConstants'; import SDKConnect, { approveHostProps } from '../SDKConnect'; import { DEFAULT_SESSION_TIMEOUT_MS } from '../SDKConnectConstants'; import DevLogger from '../utils/DevLogger'; -import DefaultPreference from 'react-native-default-preference'; function approveHost({ host, @@ -27,15 +31,10 @@ function approveHost({ if (instance.state.connected[channelId]) { instance.state.connected[channelId].lastAuthorized = approvedUntil; } - // Prevent disabled hosts from being persisted. - DefaultPreference.set( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - JSON.stringify(instance.state.approvedHosts), - ).catch((err) => { - throw err; - }); + + store.dispatch(resetConnections(instance.state.connections)); + store.dispatch(resetApprovedHosts(instance.state.approvedHosts)); } - instance.emit('refresh'); } export default approveHost; diff --git a/app/core/SDKConnect/ConnectionManagement/connectToChannel.test.ts b/app/core/SDKConnect/ConnectionManagement/connectToChannel.test.ts index 43b66b5127c..8c20b8dde0a 100644 --- a/app/core/SDKConnect/ConnectionManagement/connectToChannel.test.ts +++ b/app/core/SDKConnect/ConnectionManagement/connectToChannel.test.ts @@ -1,5 +1,3 @@ -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../AppConstants'; import { Connection, ConnectionProps } from '../Connection'; import { DEFAULT_SESSION_TIMEOUT_MS } from '../SDKConnectConstants'; import { SDKConnect } from './../SDKConnect'; @@ -226,24 +224,6 @@ describe('connectToChannel', () => { ); }); - it('should save the new connection to DefaultPreference', async () => { - mockConnection.isReady = false; - - await connectToChannel({ - instance: mockInstance, - id, - trigger, - otherPublicKey, - origin, - validUntil, - }); - - expect(DefaultPreference.set).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_CONNECTIONS, - JSON.stringify(mockInstance.state.connections), - ); - }); - it('should initiate the connection with key exchange', async () => { mockConnection.isReady = false; @@ -275,19 +255,5 @@ describe('connectToChannel', () => { expect(mockInstance.state.connecting[id]).toBe(false); }); - - it('should emit a refresh event', async () => { - mockConnection.isReady = false; - - await connectToChannel({ - instance: mockInstance, - id, - trigger, - otherPublicKey, - origin, - validUntil, - }); - expect(mockEmit).toHaveBeenCalledWith('refresh'); - }); }); }); diff --git a/app/core/SDKConnect/ConnectionManagement/connectToChannel.ts b/app/core/SDKConnect/ConnectionManagement/connectToChannel.ts index b3f665d3813..7024e27a135 100644 --- a/app/core/SDKConnect/ConnectionManagement/connectToChannel.ts +++ b/app/core/SDKConnect/ConnectionManagement/connectToChannel.ts @@ -1,5 +1,5 @@ -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../AppConstants'; +import { resetConnections } from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import { Connection, ConnectionProps } from '../Connection'; import { DEFAULT_SESSION_TIMEOUT_MS } from '../SDKConnectConstants'; import DevLogger from '../utils/DevLogger'; @@ -94,16 +94,13 @@ async function connectToChannel({ // Make sure to watch event before you connect instance.watchConnection(instance.state.connected[id]); - await DefaultPreference.set( - AppConstants.MM_SDK.SDK_CONNECTIONS, - JSON.stringify(instance.state.connections), - ); + store.dispatch(resetConnections(instance.state.connections)); + // Initialize connection instance.state.connected[id].connect({ withKeyExchange: true, }); instance.state.connecting[id] = false; - instance.emit('refresh'); } export default connectToChannel; diff --git a/app/core/SDKConnect/ConnectionManagement/invalidateChannel.test.ts b/app/core/SDKConnect/ConnectionManagement/invalidateChannel.test.ts index 014721717db..94d89889cb2 100644 --- a/app/core/SDKConnect/ConnectionManagement/invalidateChannel.test.ts +++ b/app/core/SDKConnect/ConnectionManagement/invalidateChannel.test.ts @@ -1,7 +1,6 @@ -import invalidateChannel from './invalidateChannel'; -import SDKConnect from '../SDKConnect'; import AppConstants from '../../../core/AppConstants'; -import DefaultPreference from 'react-native-default-preference'; +import SDKConnect from '../SDKConnect'; +import invalidateChannel from './invalidateChannel'; jest.mock('../../../core/AppConstants'); jest.mock('../SDKConnect'); @@ -96,46 +95,4 @@ describe('invalidateChannel', () => { expect(mockInstance.state.connections[mockChannelId]).toBe(undefined); }); - - it('should update the approved hosts list in DefaultPreference', () => { - const mockChannelId = 'mockChannelId'; - const mockApprovedUntil = 1234567890; - const mockHost = AppConstants.MM_SDK.SDK_REMOTE_ORIGIN + mockChannelId; - - mockInstance.state.approvedHosts[mockHost] = mockApprovedUntil; - mockInstance.state.connections[mockChannelId] = { - lastAuthorized: mockApprovedUntil, - } as unknown as SDKConnect['state']['connections'][string]; - - invalidateChannel({ - channelId: mockChannelId, - instance: mockInstance, - }); - - expect(DefaultPreference.set).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - JSON.stringify(mockInstance.state.approvedHosts), - ); - }); - - it('should update the connections list in DefaultPreference', () => { - const mockChannelId = 'mockChannelId'; - const mockApprovedUntil = 1234567890; - const mockHost = AppConstants.MM_SDK.SDK_REMOTE_ORIGIN + mockChannelId; - - mockInstance.state.approvedHosts[mockHost] = mockApprovedUntil; - mockInstance.state.connections[mockChannelId] = { - lastAuthorized: mockApprovedUntil, - } as unknown as SDKConnect['state']['connections'][string]; - - invalidateChannel({ - channelId: mockChannelId, - instance: mockInstance, - }); - - expect(DefaultPreference.set).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_CONNECTIONS, - JSON.stringify(mockInstance.state.connections), - ); - }); }); diff --git a/app/core/SDKConnect/ConnectionManagement/invalidateChannel.ts b/app/core/SDKConnect/ConnectionManagement/invalidateChannel.ts index b08860f4d6d..7a4c5ece1a1 100644 --- a/app/core/SDKConnect/ConnectionManagement/invalidateChannel.ts +++ b/app/core/SDKConnect/ConnectionManagement/invalidateChannel.ts @@ -1,6 +1,10 @@ +import { + resetApprovedHosts, + resetConnections, +} from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import AppConstants from '../../../core/AppConstants'; import SDKConnect from '../SDKConnect'; -import DefaultPreference from 'react-native-default-preference'; function invalidateChannel({ channelId, @@ -17,19 +21,8 @@ function invalidateChannel({ delete instance.state.connecting[channelId]; delete instance.state.connections[channelId]; - DefaultPreference.set( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - JSON.stringify(instance.state.approvedHosts), - ).catch((err) => { - throw err; - }); - - DefaultPreference.set( - AppConstants.MM_SDK.SDK_CONNECTIONS, - JSON.stringify(instance.state.connections), - ).catch((err) => { - throw err; - }); + store.dispatch(resetApprovedHosts(instance.state.approvedHosts)); + store.dispatch(resetConnections(instance.state.connections)); } export default invalidateChannel; diff --git a/app/core/SDKConnect/ConnectionManagement/reconnect.test.ts b/app/core/SDKConnect/ConnectionManagement/reconnect.test.ts index ae68eb03b09..2a0da483a01 100644 --- a/app/core/SDKConnect/ConnectionManagement/reconnect.test.ts +++ b/app/core/SDKConnect/ConnectionManagement/reconnect.test.ts @@ -35,7 +35,6 @@ describe('reconnect', () => { const mockRemoteConnect = jest.fn(); const mockRemoveChannel = jest.fn(); const mockWatchConnection = jest.fn(); - const mockEmit = jest.fn(); const mockSetTrigger = jest.fn(); const mockApproveHost = jest.fn(); const mockDisapproveChannel = jest.fn(); @@ -44,6 +43,7 @@ describe('reconnect', () => { const mockIsApproved = jest.fn(); const mockUpdateOriginatorInfos = jest.fn(); const mockReconnect = jest.fn(); + const mockUpdateSDKLoadingState = jest.fn(); beforeEach(() => { jest.clearAllMocks(); @@ -77,6 +77,7 @@ describe('reconnect', () => { rpcqueueManager: {}, navigation: {}, }, + updateSDKLoadingState: mockUpdateSDKLoadingState, _approveHost: mockApproveHost, disapproveChannel: mockDisapproveChannel, getApprovedHosts: mockGetApprovedHosts, @@ -86,7 +87,6 @@ describe('reconnect', () => { updateOriginatorInfos: mockUpdateOriginatorInfos, reconnect: mockReconnect, watchConnection: mockWatchConnection, - emit: mockEmit, } as unknown as SDKConnect; }); @@ -106,7 +106,6 @@ describe('reconnect', () => { expect(mockRemoteIsReady).toHaveBeenCalledTimes(1); expect(mockRemoteConnect).toHaveBeenCalledTimes(0); expect(mockSetTrigger).toHaveBeenCalledTimes(1); - expect(mockEmit).toHaveBeenCalledTimes(0); }); }); @@ -151,7 +150,6 @@ describe('reconnect', () => { expect(mockRemoteIsConnected).toHaveBeenCalledTimes(2); expect(mockRemoteConnect).toHaveBeenCalledTimes(0); expect(mockSetTrigger).toHaveBeenCalledTimes(0); - expect(mockEmit).toHaveBeenCalledTimes(1); }); it('should create a new connection instance if necessary', async () => { @@ -175,7 +173,6 @@ describe('reconnect', () => { mockInstance.state.connected['test-channel-id'].connect, ).toHaveBeenCalledTimes(1); expect(mockSetTrigger).toHaveBeenCalledTimes(0); - expect(mockEmit).toHaveBeenCalledTimes(1); }); it('should initiate the connection with key exchange', async () => { @@ -199,7 +196,6 @@ describe('reconnect', () => { mockInstance.state.connected['test-channel-id'].connect, ).toHaveBeenCalledTimes(1); expect(mockSetTrigger).toHaveBeenCalledTimes(0); - expect(mockEmit).toHaveBeenCalledTimes(1); }); it('should watch the new connection', async () => { @@ -238,23 +234,5 @@ describe('reconnect', () => { expect(mockInstance.state.connecting['test-channel-id']).toEqual(true); }); - - it('should emit a refresh event', async () => { - mockRemoteIsReady.mockReturnValue(false); - mockRemoteIsPaused.mockReturnValue(false); - mockRemoteIsConnected.mockReturnValue(false); - - await reconnect({ - initialConnection: false, - instance: mockInstance, - channelId: 'test-channel-id', - trigger: 'deeplink', - otherPublicKey: 'test-other-public-key', - context: 'test-context', - }); - - expect(mockEmit).toHaveBeenCalledTimes(1); - expect(mockEmit).toHaveBeenCalledWith('refresh'); - }); }); }); diff --git a/app/core/SDKConnect/ConnectionManagement/reconnect.ts b/app/core/SDKConnect/ConnectionManagement/reconnect.ts index ad3bde83612..d83a244896b 100644 --- a/app/core/SDKConnect/ConnectionManagement/reconnect.ts +++ b/app/core/SDKConnect/ConnectionManagement/reconnect.ts @@ -29,6 +29,7 @@ async function reconnect({ if (trigger) { instance.state.connected[channelId].setTrigger('deeplink'); } + instance.updateSDKLoadingState({ channelId, loading: false }); return; } @@ -116,6 +117,7 @@ async function reconnect({ DevLogger.log( `SDKConnect::reconnect - already connected [connected] -- trigger updated to '${trigger}'`, ); + instance.updateSDKLoadingState({ channelId, loading: false }); return; } @@ -123,6 +125,7 @@ async function reconnect({ DevLogger.log( `SDKConnect::reconnect - already connected [ready=${ready}] -- ignoring`, ); + instance.updateSDKLoadingState({ channelId, loading: false }); return; } } @@ -154,11 +157,11 @@ async function reconnect({ instance.state.connected[channelId].connect({ withKeyExchange: true, }); + instance.watchConnection(instance.state.connected[channelId]); const afterConnected = instance.state.connected[channelId].remote.isConnected() ?? false; instance.state.connecting[channelId] = !afterConnected; // If not connected, it means it's connecting. - instance.emit('refresh'); } export default reconnect; diff --git a/app/core/SDKConnect/ConnectionManagement/reconnectAll.ts b/app/core/SDKConnect/ConnectionManagement/reconnectAll.ts index 423b41f3947..0e28957d765 100644 --- a/app/core/SDKConnect/ConnectionManagement/reconnectAll.ts +++ b/app/core/SDKConnect/ConnectionManagement/reconnectAll.ts @@ -14,9 +14,13 @@ async function reconnectAll(instance: SDKConnect) { } const channelIds = Object.keys(instance.state.connections); + channelIds.forEach((channelId) => { // Only reconnects to type 'qrcode' connections. const connection = instance.state.connections[channelId]; + DevLogger.log( + `SDKConnect::reconnectAll - reconnecting to ${channelId} origin=${connection.origin}`, + ); if (connection.origin === AppConstants.DEEPLINKS.ORIGIN_QR_CODE) { instance .reconnect({ @@ -35,7 +39,9 @@ async function reconnectAll(instance: SDKConnect) { } }); instance.state.reconnected = true; - DevLogger.log(`SDKConnect::reconnectAll - done`); + DevLogger.log( + `SDKConnect::reconnectAll - channelIds=${channelIds.length} - done`, + ); } export default reconnectAll; diff --git a/app/core/SDKConnect/ConnectionManagement/removeAll.test.ts b/app/core/SDKConnect/ConnectionManagement/removeAll.test.ts index 655a8c1fd94..79a34f93e47 100644 --- a/app/core/SDKConnect/ConnectionManagement/removeAll.test.ts +++ b/app/core/SDKConnect/ConnectionManagement/removeAll.test.ts @@ -1,7 +1,5 @@ -import removeAll from './removeAll'; import SDKConnect from '../SDKConnect'; -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../AppConstants'; +import removeAll from './removeAll'; jest.mock('react-native-default-preference', () => ({ set: jest.fn().mockResolvedValue([]), @@ -44,19 +42,10 @@ describe('removeAll', () => { expect(mockRemoveChannel).toHaveBeenCalledWith({ channelId: mockChannelId, - emitRefresh: false, sendTerminate: true, }); }); - it('should clear all android connections from DefaultPreference', async () => { - await removeAll(mockInstance); - - expect(DefaultPreference.clear).toHaveBeenCalledWith( - AppConstants.MM_SDK.ANDROID_CONNECTIONS, - ); - }); - describe('Resetting state properties', () => { it('should reset approved hosts', async () => { await removeAll(mockInstance); @@ -94,20 +83,4 @@ describe('removeAll', () => { expect(mockInstance.state.paused).toBe(false); }); }); - - it('should clear SDK connections from DefaultPreference', async () => { - await removeAll(mockInstance); - - expect(DefaultPreference.clear).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_CONNECTIONS, - ); - }); - - it('should clear approved hosts from DefaultPreference', async () => { - await removeAll(mockInstance); - - expect(DefaultPreference.clear).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - ); - }); }); diff --git a/app/core/SDKConnect/ConnectionManagement/removeAll.ts b/app/core/SDKConnect/ConnectionManagement/removeAll.ts index a1552ecf7d1..6a984a79c8f 100644 --- a/app/core/SDKConnect/ConnectionManagement/removeAll.ts +++ b/app/core/SDKConnect/ConnectionManagement/removeAll.ts @@ -1,5 +1,9 @@ -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../AppConstants'; +import { + resetAndroidConnections, + resetApprovedHosts, + resetConnections, +} from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import SDKConnect from '../SDKConnect'; async function removeAll(instance: SDKConnect) { @@ -7,7 +11,6 @@ async function removeAll(instance: SDKConnect) { instance.removeChannel({ channelId: id, sendTerminate: true, - emitRefresh: false, }); } @@ -15,13 +18,9 @@ async function removeAll(instance: SDKConnect) { instance.removeChannel({ channelId: id, sendTerminate: true, - emitRefresh: false, }); } - // Remove all android connections - await DefaultPreference.clear(AppConstants.MM_SDK.ANDROID_CONNECTIONS); - // Also remove approved hosts that may have been skipped. instance.state.approvedHosts = {}; instance.state.disabledHosts = {}; @@ -30,11 +29,9 @@ async function removeAll(instance: SDKConnect) { instance.state.connecting = {}; instance.state.paused = false; - await DefaultPreference.clear(AppConstants.MM_SDK.SDK_CONNECTIONS); - await DefaultPreference.clear(AppConstants.MM_SDK.SDK_APPROVEDHOSTS); - - // Delayed ui refresh - setTimeout(() => instance.emit('refresh'), 100); + store.dispatch(resetConnections({})); + store.dispatch(resetApprovedHosts({})); + store.dispatch(resetAndroidConnections({})); } export default removeAll; diff --git a/app/core/SDKConnect/ConnectionManagement/removeChannel.test.ts b/app/core/SDKConnect/ConnectionManagement/removeChannel.test.ts index 9eff6d93ab5..c6a63fa14a5 100644 --- a/app/core/SDKConnect/ConnectionManagement/removeChannel.test.ts +++ b/app/core/SDKConnect/ConnectionManagement/removeChannel.test.ts @@ -1,4 +1,3 @@ -import DefaultPreference from 'react-native-default-preference'; import AppConstants from '../../../core/AppConstants'; import SDKConnect from '../SDKConnect'; import removeChannel from './removeChannel'; @@ -25,7 +24,6 @@ describe('removeChannel', () => { disabledHosts: {}, connecting: {}, }, - emit: jest.fn(), } as unknown as SDKConnect; }); @@ -115,50 +113,6 @@ describe('removeChannel', () => { expect(mockInstance.state.disabledHosts[mockHost]).toBeUndefined(); }); - - it('should update connections in DefaultPreference', () => { - const mockChannelId = 'mockChannelId'; - const mockOtherPublicKey = 'mockOtherPublicKey'; - const mockHost = AppConstants.MM_SDK.SDK_REMOTE_ORIGIN + mockChannelId; - - mockInstance.state.connected[mockChannelId] = { - otherPublicKey: mockOtherPublicKey, - } as unknown as SDKConnect['state']['connected'][string]; - - mockInstance.state.disabledHosts[mockHost] = 1234567890; - - removeChannel({ - channelId: mockChannelId, - instance: mockInstance, - }); - - expect(DefaultPreference.set).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_CONNECTIONS, - JSON.stringify(mockInstance.state.connections), - ); - }); - - it('should update approved hosts in DefaultPreference', () => { - const mockChannelId = 'mockChannelId'; - const mockOtherPublicKey = 'mockOtherPublicKey'; - const mockHost = AppConstants.MM_SDK.SDK_REMOTE_ORIGIN + mockChannelId; - - mockInstance.state.connected[mockChannelId] = { - otherPublicKey: mockOtherPublicKey, - } as unknown as SDKConnect['state']['connected'][string]; - - mockInstance.state.disabledHosts[mockHost] = 1234567890; - - removeChannel({ - channelId: mockChannelId, - instance: mockInstance, - }); - - expect(DefaultPreference.set).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - JSON.stringify(mockInstance.state.approvedHosts), - ); - }); }); it('should delete the channel from the connecting state', () => { @@ -173,15 +127,4 @@ describe('removeChannel', () => { expect(mockInstance.state.connecting[mockChannelId]).toBeUndefined(); }); - - it('should emit a refresh event', () => { - const mockChannelId = 'mockChannelId'; - - removeChannel({ - channelId: mockChannelId, - instance: mockInstance, - }); - - expect(mockInstance.emit).toHaveBeenCalledWith('refresh'); - }); }); diff --git a/app/core/SDKConnect/ConnectionManagement/removeChannel.ts b/app/core/SDKConnect/ConnectionManagement/removeChannel.ts index 5dc3ed85feb..7354721edc1 100644 --- a/app/core/SDKConnect/ConnectionManagement/removeChannel.ts +++ b/app/core/SDKConnect/ConnectionManagement/removeChannel.ts @@ -1,13 +1,16 @@ +import { PermissionController } from '@metamask/permission-controller'; +import { + removeApprovedHost, + removeConnection, +} from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import AppConstants from '../../../core/AppConstants'; +import Engine from '../../Engine'; import SDKConnect from '../SDKConnect'; import DevLogger from '../utils/DevLogger'; -import DefaultPreference from 'react-native-default-preference'; -import Engine from '../../Engine'; -import { PermissionController } from '@metamask/permission-controller'; function removeChannel({ channelId, - emitRefresh = true, engine, sendTerminate, instance, @@ -15,7 +18,6 @@ function removeChannel({ channelId: string; engine?: typeof Engine; sendTerminate?: boolean; - emitRefresh?: boolean; instance: SDKConnect; }) { // check if it is an android sdk connection, if it doesn't belong to regular connections @@ -52,19 +54,8 @@ function removeChannel({ AppConstants.MM_SDK.SDK_REMOTE_ORIGIN + channelId ]; - DefaultPreference.set( - AppConstants.MM_SDK.SDK_CONNECTIONS, - JSON.stringify(instance.state.connections), - ).catch((err) => { - throw err; - }); - - DefaultPreference.set( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - JSON.stringify(instance.state.approvedHosts), - ).catch((err) => { - throw err; - }); + store.dispatch(removeConnection(channelId)); + store.dispatch(removeApprovedHost(channelId)); } // Remove matching permissions from controller @@ -77,10 +68,6 @@ function removeChannel({ permissionsController.revokeAllPermissions(channelId); } } - - if (emitRefresh) { - instance.emit('refresh'); - } } export default removeChannel; diff --git a/app/core/SDKConnect/ConnectionManagement/watchConnection.test.ts b/app/core/SDKConnect/ConnectionManagement/watchConnection.test.ts index 375b2e3b9ef..d9d71378b0f 100644 --- a/app/core/SDKConnect/ConnectionManagement/watchConnection.test.ts +++ b/app/core/SDKConnect/ConnectionManagement/watchConnection.test.ts @@ -68,7 +68,6 @@ describe('watchConnection', () => { expect(mockRemoveChannel).toHaveBeenCalledWith({ channelId: mockConnection.channelId, - emitRefresh: true, sendTerminate: false, }); }); diff --git a/app/core/SDKConnect/ConnectionManagement/watchConnection.ts b/app/core/SDKConnect/ConnectionManagement/watchConnection.ts index ebb664a013f..a9b66064b52 100644 --- a/app/core/SDKConnect/ConnectionManagement/watchConnection.ts +++ b/app/core/SDKConnect/ConnectionManagement/watchConnection.ts @@ -1,4 +1,6 @@ import { ConnectionStatus, EventType } from '@metamask/sdk-communication-layer'; +import { resetConnections } from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import Logger from '../../../util/Logger'; import AppConstants from '../../AppConstants'; import { Connection } from '../Connection'; @@ -13,20 +15,20 @@ function watchConnection(connection: Connection, instance: SDKConnect) { if (connectionStatus === ConnectionStatus.TERMINATED) { instance.removeChannel({ channelId: connection.channelId, - emitRefresh: true, sendTerminate: false, }); + instance.state.connections[connection.channelId].connected = false; } else if (connectionStatus === ConnectionStatus.DISCONNECTED) { instance.updateSDKLoadingState({ channelId: connection.channelId, loading: false, }); + instance.state.connections[connection.channelId].connected = false; } + store.dispatch(resetConnections(instance.state.connections)); DevLogger.log( `SDKConnect::watchConnection CONNECTION_STATUS ${connection.channelId} ${connectionStatus}`, ); - // Inform ui about connection status change - instance.emit('refresh'); }, ); @@ -37,8 +39,12 @@ function watchConnection(connection: Connection, instance: SDKConnect) { DevLogger.log( `SDKConnect::watchConnection CLIENTS_DISCONNECTED channel=${connection.channelId} origin=${connection.origin} isDisabled=${isDisabled}`, ); - // Always update initialConnection state - instance.state.connections[connection.channelId].initialConnection = false; + + // update initialConnection state + if (instance.state.connections[connection.channelId]) { + instance.state.connections[connection.channelId].initialConnection = + false; + } if (isDisabled !== undefined) { instance diff --git a/app/core/SDKConnect/InitializationManagement/asyncInit.test.ts b/app/core/SDKConnect/InitializationManagement/asyncInit.test.ts index 5d314a91548..ddc40c210f8 100644 --- a/app/core/SDKConnect/InitializationManagement/asyncInit.test.ts +++ b/app/core/SDKConnect/InitializationManagement/asyncInit.test.ts @@ -1,6 +1,4 @@ import { NavigationContainerRef } from '@react-navigation/native'; -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../AppConstants'; import SDKConnect from '../SDKConnect'; import { wait } from '../utils/wait.util'; import asyncInit from './asyncInit'; @@ -72,18 +70,6 @@ describe('asyncInit', () => { }); describe('Loading connections and hosts from storage', () => { - it('should load connections and approved hosts from DefaultPreference', async () => { - await asyncInit({ - instance: mockInstance, - navigation: mockNavigation, - }); - - expect(DefaultPreference.get).toHaveBeenCalledTimes(2); - expect(DefaultPreference.get).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_CONNECTIONS, - ); - }); - it('should parse and set connections from the storage', async () => { await asyncInit({ instance: mockInstance, diff --git a/app/core/SDKConnect/InitializationManagement/asyncInit.ts b/app/core/SDKConnect/InitializationManagement/asyncInit.ts index 2840264c689..5089b2ed085 100644 --- a/app/core/SDKConnect/InitializationManagement/asyncInit.ts +++ b/app/core/SDKConnect/InitializationManagement/asyncInit.ts @@ -1,8 +1,9 @@ import { NavigationContainerRef } from '@react-navigation/native'; -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../AppConstants'; +import { RootState } from '../../../../app/reducers'; +import { disconnectAll } from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import Logger from '../../../util/Logger'; -import SDKConnect, { ApprovedHosts } from '../SDKConnect'; +import SDKConnect, { ApprovedHosts, SDKSessions } from '../SDKConnect'; import DevLogger from '../utils/DevLogger'; import { wait } from '../utils/wait.util'; @@ -25,56 +26,38 @@ const asyncInit = async ({ await wait(1000); DevLogger.log(`SDKConnect::init() - waited 1000ms - keep initializing`); - try { - DevLogger.log(`SDKConnect::init() - loading connections`); - // On Android the DefaultPreferences will start loading after the biometrics - const [connectionsStorage, hostsStorage] = await Promise.all([ - DefaultPreference.get(AppConstants.MM_SDK.SDK_CONNECTIONS), - DefaultPreference.get(AppConstants.MM_SDK.SDK_APPROVEDHOSTS), - ]); - - DevLogger.log( - `SDKConnect::init() - connectionsStorage=${connectionsStorage} hostsStorage=${hostsStorage}`, - ); - - if (connectionsStorage) { - instance.state.connections = JSON.parse(connectionsStorage); - DevLogger.log( - `SDKConnect::init() - connections [${ - Object.keys(instance.state.connections).length - }]`, - ); - } + // All connectectiions are disconnected on start + store.dispatch(disconnectAll()); - if (hostsStorage) { - const uncheckedHosts = JSON.parse(hostsStorage) as ApprovedHosts; - // Check if the approved hosts haven't timed out. - const approvedHosts: ApprovedHosts = {}; - let expiredCounter = 0; - for (const host in uncheckedHosts) { - const expirationTime = uncheckedHosts[host]; - if (Date.now() < expirationTime) { - // Host is valid, add it to the list. - approvedHosts[host] = expirationTime; - } else { - expiredCounter += 1; - } - } - if (expiredCounter > 1) { - // Update the list of approved hosts excluding the expired ones. - await DefaultPreference.set( - AppConstants.MM_SDK.SDK_APPROVEDHOSTS, - JSON.stringify(approvedHosts), + const { sdk } = store.getState() as RootState; + const validConnections: SDKSessions = {}; + const validHosts: ApprovedHosts = {}; + try { + // Remove connections that have expired. + const now = Date.now(); + for (const id in sdk.connections) { + const connInfo = sdk.connections[id]; + if (sdk.approvedHosts[id] <= now) { + // Only keep connections that are not expired. + validConnections[id] = sdk.connections[id]; + validHosts[id] = sdk.approvedHosts[id]; + } else { + // Remove expired connections + DevLogger.log( + `SDKConnect::init() - removing expired connection ${id}`, + connInfo, ); } - instance.state.approvedHosts = approvedHosts; - DevLogger.log( - `SDKConnect::init() - approvedHosts [${ - Object.keys(instance.state.approvedHosts).length - }]`, - ); } + instance.state.connections = sdk.connections; + instance.state.approvedHosts = sdk.approvedHosts; + instance.state.androidConnections = sdk.androidConnections; + // Update store with valid connection + store.dispatch({ + type: 'SDK_SET_CONNECTIONS', + payload: validConnections, + }); DevLogger.log(`SDKConnect::init() - done`); instance.state._initialized = true; } catch (err) { diff --git a/app/core/SDKConnect/SDKConnect.ts b/app/core/SDKConnect/SDKConnect.ts index 1ded693f121..89735987284 100644 --- a/app/core/SDKConnect/SDKConnect.ts +++ b/app/core/SDKConnect/SDKConnect.ts @@ -4,7 +4,7 @@ import AppConstants from '../AppConstants'; import { OriginatorInfo } from '@metamask/sdk-communication-layer'; import { NavigationContainerRef } from '@react-navigation/native'; -import { EventEmitter2 } from 'eventemitter2'; +import Engine from '../../core/Engine'; import AndroidService from './AndroidSDK/AndroidService'; import addAndroidConnection from './AndroidSDK/addAndroidConnection'; import bindAndroidSDK from './AndroidSDK/bindAndroidSDK'; @@ -32,7 +32,6 @@ import { updateSDKLoadingState, } from './StateManagement'; import DevLogger from './utils/DevLogger'; -import Engine from '../../core/Engine'; export interface ConnectedSessions { [id: string]: Connection; @@ -85,7 +84,7 @@ export interface SDKConnectState { export type SDKEventListener = (event: string) => void; -export class SDKConnect extends EventEmitter2 { +export class SDKConnect { private static instance: SDKConnect; public state: SDKConnectState = { @@ -254,18 +253,15 @@ export class SDKConnect extends EventEmitter2 { public removeChannel({ channelId, sendTerminate, - emitRefresh, }: { channelId: string; sendTerminate?: boolean; - emitRefresh?: boolean; }) { return removeChannel({ channelId, engine: Engine, sendTerminate, instance: this, - emitRefresh, }); } diff --git a/app/core/SDKConnect/SessionManagement/pause.ts b/app/core/SDKConnect/SessionManagement/pause.ts index a70112461c1..0e8ebbc8bc5 100644 --- a/app/core/SDKConnect/SessionManagement/pause.ts +++ b/app/core/SDKConnect/SessionManagement/pause.ts @@ -1,5 +1,7 @@ +import { disconnectAll } from '../../../../app/actions/sdk'; import SDKConnect from '../SDKConnect'; import DevLogger from '../utils/DevLogger'; +import { store } from '../../../../app/store'; function pause(instance: SDKConnect) { if (instance.state.paused) return; @@ -20,6 +22,8 @@ function pause(instance: SDKConnect) { } instance.state.paused = true; instance.state.connecting = {}; + // Set disconnected status for all connections + store.dispatch(disconnectAll()); } export default pause; diff --git a/app/core/SDKConnect/StateManagement/hideLoadingState.ts b/app/core/SDKConnect/StateManagement/hideLoadingState.ts index 1319655fa5c..55ab82fb4e4 100644 --- a/app/core/SDKConnect/StateManagement/hideLoadingState.ts +++ b/app/core/SDKConnect/StateManagement/hideLoadingState.ts @@ -1,14 +1,20 @@ import Routes from '../../../constants/navigation/Routes'; import SDKConnect from '../SDKConnect'; +import DevLogger from '../utils/DevLogger'; async function hideLoadingState({ instance }: { instance: SDKConnect }) { instance.state.sdkLoadingState = {}; const currentRoute = instance.state.navigation?.getCurrentRoute()?.name; + DevLogger.log(`SDKConnect::hideLoadingState currentRoute=${currentRoute}`); if ( currentRoute === Routes.SHEET.SDK_LOADING && instance.state.navigation?.canGoBack() ) { instance.state.navigation?.goBack(); + } else { + DevLogger.log( + `SDKConnect::hideLoadingState - SKIP - currentRoute=${currentRoute}`, + ); } } diff --git a/app/core/SDKConnect/StateManagement/updateOriginatorInfos.test.ts b/app/core/SDKConnect/StateManagement/updateOriginatorInfos.test.ts index 2c3038568ec..3614abd64ab 100644 --- a/app/core/SDKConnect/StateManagement/updateOriginatorInfos.test.ts +++ b/app/core/SDKConnect/StateManagement/updateOriginatorInfos.test.ts @@ -1,6 +1,5 @@ import { OriginatorInfo } from '@metamask/sdk-communication-layer'; import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../AppConstants'; import SDKConnect from '../SDKConnect'; import updateOriginatorInfos from './updateOriginatorInfos'; @@ -65,43 +64,4 @@ describe('updateOriginatorInfos', () => { mockOriginatorInfo, ); }); - - it('should update the connection in DefaultPreference', () => { - const mockChannelId = 'mockChannelId'; - - const mockOriginatorInfo = {} as OriginatorInfo; - - mockInstance.state.connections[mockChannelId] = { - originatorInfo: {} as OriginatorInfo, - } as any; - - updateOriginatorInfos({ - channelId: mockChannelId, - originatorInfo: mockOriginatorInfo, - instance: mockInstance, - }); - - expect(mockDefaultPreferenceSet).toHaveBeenCalledWith( - AppConstants.MM_SDK.SDK_CONNECTIONS, - JSON.stringify(mockInstance.state.connections), - ); - }); - - it('should emit a refresh event', () => { - const mockChannelId = 'mockChannelId'; - - const mockOriginatorInfo = {} as OriginatorInfo; - - mockInstance.state.connections[mockChannelId] = { - originatorInfo: {} as OriginatorInfo, - } as any; - - updateOriginatorInfos({ - channelId: mockChannelId, - originatorInfo: mockOriginatorInfo, - instance: mockInstance, - }); - - expect(mockEmit).toHaveBeenCalledWith('refresh'); - }); }); diff --git a/app/core/SDKConnect/StateManagement/updateOriginatorInfos.ts b/app/core/SDKConnect/StateManagement/updateOriginatorInfos.ts index 23271fd38b5..4934d1d412d 100644 --- a/app/core/SDKConnect/StateManagement/updateOriginatorInfos.ts +++ b/app/core/SDKConnect/StateManagement/updateOriginatorInfos.ts @@ -1,7 +1,8 @@ import { OriginatorInfo } from '@metamask/sdk-communication-layer'; +import { resetConnections } from '../../../../app/actions/sdk'; +import { store } from '../../../../app/store'; import SDKConnect from '../SDKConnect'; -import DefaultPreference from 'react-native-default-preference'; -import AppConstants from '../../AppConstants'; +import DevLogger from '../utils/DevLogger'; function updateOriginatorInfos({ channelId, @@ -17,14 +18,18 @@ function updateOriginatorInfos({ return; } - instance.state.connections[channelId].originatorInfo = originatorInfo; - DefaultPreference.set( - AppConstants.MM_SDK.SDK_CONNECTIONS, - JSON.stringify(instance.state.connections), - ).catch((err) => { - throw err; - }); - instance.emit('refresh'); + // update originatorInfo + instance.state.connections[channelId] = { + ...instance.state.connections[channelId], + originatorInfo, + connected: true, + }; + + DevLogger.log( + `SDKConnect::updateOriginatorInfos`, + instance.state.connections, + ); + store.dispatch(resetConnections(instance.state.connections)); } export default updateOriginatorInfos; diff --git a/app/core/SDKConnect/handlers/checkPermissions.test.ts b/app/core/SDKConnect/handlers/checkPermissions.test.ts index 23e140dc815..4e82c66862c 100644 --- a/app/core/SDKConnect/handlers/checkPermissions.test.ts +++ b/app/core/SDKConnect/handlers/checkPermissions.test.ts @@ -27,6 +27,7 @@ describe('checkPermissions', () => { let permissionController = { executeProviderRequest: jest.fn(), executeRestrictedMethod: jest.fn().mockResolvedValue({}), + hasPermissions: jest.fn(), requestPermissions, } as unknown as PermissionController; const HOUR_IN_MS = 3600000; @@ -65,6 +66,7 @@ describe('checkPermissions', () => { permissionController = { executeProviderRequest: jest.fn(), executeRestrictedMethod: jest.fn().mockResolvedValue({}), + hasPermissions: jest.fn(), requestPermissions, } as unknown as PermissionController; diff --git a/app/core/SDKConnect/handlers/checkPermissions.ts b/app/core/SDKConnect/handlers/checkPermissions.ts index 143b1d394b6..8f0c4926f3b 100644 --- a/app/core/SDKConnect/handlers/checkPermissions.ts +++ b/app/core/SDKConnect/handlers/checkPermissions.ts @@ -51,6 +51,9 @@ export const checkPermissions = async ({ if (connection.approvalPromise) { DevLogger.log(`checkPermissions approvalPromise exists`); + // Make sure the window is displayed. + const match = permissionsController.hasPermissions(connection.channelId); + DevLogger.log(`checkPermissions match`, match); // Wait for result and clean the promise afterwards. await connection.approvalPromise; connection.approvalPromise = undefined; diff --git a/app/core/WalletConnect/WalletConnectV2.ts b/app/core/WalletConnect/WalletConnectV2.ts index 7c0dc961719..cd908115303 100644 --- a/app/core/WalletConnect/WalletConnectV2.ts +++ b/app/core/WalletConnect/WalletConnectV2.ts @@ -1,18 +1,15 @@ import AppConstants from '../AppConstants'; import BackgroundBridge from '../BackgroundBridge/BackgroundBridge'; import { Minimizer } from '../NativeModules'; -import getRpcMethodMiddleware, { - ApprovalTypes, -} from '../RPCMethods/RPCMethodMiddleware'; +import getRpcMethodMiddleware from '../RPCMethods/RPCMethodMiddleware'; -import { ApprovalController } from '@metamask/approval-controller'; import { KeyringController } from '@metamask/keyring-controller'; import { PreferencesController } from '@metamask/preferences-controller'; import Logger from '../../util/Logger'; import { WalletDevice } from '@metamask/transaction-controller'; -import AsyncStorage from '../../store/async-storage-wrapper'; +import { PermissionController } from '@metamask/permission-controller'; import { Core } from '@walletconnect/core'; import { ErrorResponse } from '@walletconnect/jsonrpc-types'; import Client, { @@ -21,7 +18,15 @@ import Client, { } from '@walletconnect/se-sdk'; import { SessionTypes } from '@walletconnect/types'; import { getSdkError } from '@walletconnect/utils'; +import ppomUtil from '../../../app/lib/ppom/ppom-util'; +import { WALLET_CONNECT_ORIGIN } from '../../../app/util/walletconnect'; +import { selectChainId } from '../../selectors/networkController'; +import { store } from '../../store'; +import AsyncStorage from '../../store/async-storage-wrapper'; +import { addTransaction } from '../../util/transaction-controller'; import Engine from '../Engine'; +import { getPermittedAccounts } from '../Permissions'; +import DevLogger from '../SDKConnect/utils/DevLogger'; import getAllUrlParams from '../SDKConnect/utils/getAllUrlParams.util'; import { waitForKeychainUnlocked } from '../SDKConnect/utils/wait.util'; import WalletConnect from './WalletConnect'; @@ -29,11 +34,7 @@ import METHODS_TO_REDIRECT from './wc-config'; import parseWalletConnectUri, { waitForNetworkModalOnboarding, } from './wc-utils'; -import { selectChainId } from '../../selectors/networkController'; -import { store } from '../../store'; -import { WALLET_CONNECT_ORIGIN } from '../../../app/util/walletconnect'; -import ppomUtil from '../../../app/lib/ppom/ppom-util'; -import { addTransaction } from '../../util/transaction-controller'; +import { updateWC2Metadata } from '../../../app/actions/sdk'; const { PROJECT_ID } = AppConstants.WALLET_CONNECT; export const isWC2Enabled = @@ -67,9 +68,11 @@ class WalletConnect2Session { constructor({ web3Wallet, session, + channelId, deeplink, }: { web3Wallet: Client; + channelId: string; session: SessionTypes.Struct; deeplink: boolean; }) { @@ -81,11 +84,14 @@ class WalletConnect2Session { const name = session.self.metadata.name; const icons = session.self.metadata.icons; + DevLogger.log( + `WalletConnect2Session::constructor topic=${session.topic} pairingTopic=${session.pairingTopic}`, + ); this.backgroundBridge = new BackgroundBridge({ webview: null, - channelId: session.pairingTopic, url, isWalletConnect: true, + channelId, wcRequestActions: { approveRequest: this.approveRequest.bind(this), rejectRequest: this.rejectRequest.bind(this), @@ -100,6 +106,7 @@ class WalletConnect2Session { getRpcMethodMiddleware({ hostname: url, getProviderState, + channelId, setApprovedHosts: () => false, getApprovedHosts: () => false, analytics: {}, @@ -387,6 +394,7 @@ export class WC2Manager { this.sessions[sessionKey] = new WalletConnect2Session({ web3Wallet, + channelId: sessionKey, deeplink: typeof deeplinkSessions[session.pairingTopic] !== 'undefined', session, @@ -550,41 +558,32 @@ export class WC2Manager { async onSessionProposal(proposal: SingleEthereumTypes.SessionProposal) { // Open session proposal modal for confirmation / rejection const { id, params } = proposal; - const { - proposer, - // requiredNamespaces, - // optionalNamespaces, - // sessionProperties, - // relays, - } = params; - - Logger.log(`WC2::session_proposal id=${id}`, params); - const url = proposer.metadata.url ?? ''; - const name = proposer.metadata.description ?? ''; - const icons = proposer.metadata.icons; - - const approvalController = ( - Engine.context as { ApprovalController: ApprovalController } - ).ApprovalController; + + const pairingTopic = proposal.params.pairingTopic; + DevLogger.log( + `WC2::session_proposal id=${id} pairingTopic=${pairingTopic}`, + params, + ); + + const permissionsController = ( + Engine.context as { PermissionController: PermissionController } + ).PermissionController; + + const { proposer } = params; + const { metadata } = proposer; + const url = metadata.url ?? ''; + const name = metadata.description ?? ''; + const icons = metadata.icons; + const icon = icons?.[0] ?? ''; + // Save Connection info to redux store to be retrieved in ui. + store.dispatch(updateWC2Metadata({ url, name, icon, id: `${id}` })); try { - await approvalController.add({ - id: `${id}`, - origin: url, - requestData: { - hostname: url, - peerMeta: { - url, - name, - icons, - analytics: { - request_source: AppConstants.REQUEST_SOURCES.WC2, - request_platform: '', // FIXME use mobile for deeplink or QRCODE - }, - }, - }, - type: ApprovalTypes.WALLET_CONNECT, - }); + await permissionsController.requestPermissions( + { origin: `${id}` }, + { eth_accounts: {} }, + { id: `${id}` }, + ); // Permissions approved. } catch (err) { // Failed permissions request - reject session @@ -595,18 +594,16 @@ export class WC2Manager { } try { - const preferencesController = ( - Engine.context as { PreferencesController: PreferencesController } - ).PreferencesController; + // use Permission controller + const approvedAccounts = await getPermittedAccounts(proposal.id + ''); - const selectedAddress = preferencesController.state.selectedAddress; // TODO: Misleading variable name, this is not the chain ID. This should be updated to use the chain ID. const chainId = selectChainId(store.getState()); const activeSession = await this.web3Wallet.approveSession({ id: proposal.id, chainId: parseInt(chainId), - accounts: [selectedAddress], + accounts: approvedAccounts, }); const deeplink = @@ -614,6 +611,7 @@ export class WC2Manager { 'undefined'; const session = new WalletConnect2Session({ session: activeSession, + channelId: '' + proposal.id, deeplink, web3Wallet: this.web3Wallet, }); diff --git a/app/lib/ppom/blockaid-version.js b/app/lib/ppom/blockaid-version.js index 40a10686f94..9828b7c961c 100644 --- a/app/lib/ppom/blockaid-version.js +++ b/app/lib/ppom/blockaid-version.js @@ -1 +1 @@ -var e={d:(o,r)=>{for(var t in r)e.o(r,t)&&!e.o(o,t)&&Object.defineProperty(o,t,{enumerable:!0,get:r[t]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o)},o={};e.d(o,{Z:()=>r});const r={BlockaidVersion:"1.4.2"};var t=o.Z;export{t as default}; \ No newline at end of file +var e={d:(o,r)=>{for(var t in r)e.o(r,t)&&!e.o(o,t)&&Object.defineProperty(o,t,{enumerable:!0,get:r[t]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o)},o={};e.d(o,{Z:()=>r});const r={BlockaidVersion:"1.4.4"};var t=o.Z;export{t as default}; \ No newline at end of file diff --git a/app/lib/ppom/ppom.html.js b/app/lib/ppom/ppom.html.js index 133e94653d3..22244c23ce0 100644 --- a/app/lib/ppom/ppom.html.js +++ b/app/lib/ppom/ppom.html.js @@ -1 +1 @@ -var A={d:(g,I)=>{for(var B in I)A.o(I,B)&&!A.o(g,B)&&Object.defineProperty(g,B,{enumerable:!0,get:I[B]})},o:(A,g)=>Object.prototype.hasOwnProperty.call(A,g)},g={};A.d(g,{d:()=>I});const I='Webpack App