-
Notifications
You must be signed in to change notification settings - Fork 62
feat: unite connection requests #1923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,88 @@ | ||
import { createRainbowStore } from '~/core/state/internal/createRainbowStore'; | ||
|
||
import { ProviderRequestPayload } from '../../transports/providerRequestTransport'; | ||
import type { ProviderRequestPayload } from '../../transports/providerRequestTransport'; | ||
|
||
import { getSenderHost, getTabIdString } from './utils'; | ||
|
||
type PromiseResolver = { | ||
resolve: (value: object) => void; | ||
reject: (error: Error) => void; | ||
}; | ||
|
||
export interface PendingRequestsStore { | ||
pendingRequests: ProviderRequestPayload[]; | ||
addPendingRequest: (request: ProviderRequestPayload) => void; | ||
connectionPromiseResolvers: Map<string, PromiseResolver[]>; | ||
addPendingRequest: (request: ProviderRequestPayload) => boolean; | ||
removePendingRequest: (id: number) => void; | ||
addConnectionResolver: (host: string, resolver: PromiseResolver) => void; | ||
resolveConnectionRequests: ( | ||
host: string, | ||
result: unknown, | ||
isError?: boolean, | ||
) => void; | ||
} | ||
|
||
export const usePendingRequestStore = createRainbowStore<PendingRequestsStore>( | ||
(set, get) => ({ | ||
pendingRequests: [], | ||
connectionPromiseResolvers: new Map(), | ||
addPendingRequest: (newRequest) => { | ||
// Check for duplicate eth_requestAccounts requests from the same host | ||
if (newRequest.method === 'eth_requestAccounts') { | ||
const tabId = getTabIdString(newRequest); | ||
const senderHost = getSenderHost(newRequest); | ||
if (senderHost && tabId) { | ||
// Check if there's already a pending connection request for this host | ||
const existingConnectionRequest = get().pendingRequests.find( | ||
(request) => { | ||
if (request.method !== 'eth_requestAccounts') return false; | ||
const existingHost = getSenderHost(request); | ||
const existingTabId = getTabIdString(request); | ||
return existingHost === senderHost && existingTabId === tabId; | ||
}, | ||
); | ||
|
||
if (existingConnectionRequest) { | ||
// Don't add duplicate - return false to indicate this is a duplicate | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
const pendingRequests = get().pendingRequests; | ||
set({ pendingRequests: pendingRequests.concat([newRequest]) }); | ||
return true; | ||
}, | ||
removePendingRequest: (id) => { | ||
const pendingRequests = get().pendingRequests; | ||
set({ | ||
pendingRequests: pendingRequests.filter((request) => request.id !== id), | ||
}); | ||
}, | ||
addConnectionResolver: (host, resolver) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add some notes about the resolver paradigm used here? Trying to wrap my head around the goal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added some in code comments :) Basically it's needed to allow multiple connect requests to resolve when one dialog gets accepted/declined |
||
const resolvers = get().connectionPromiseResolvers; | ||
const existingResolvers = resolvers.get(host) || []; | ||
resolvers.set(host, [...existingResolvers, resolver]); | ||
set({ connectionPromiseResolvers: new Map(resolvers) }); | ||
}, | ||
resolveConnectionRequests: (host, result, isError = false) => { | ||
const resolvers = get().connectionPromiseResolvers; | ||
const hostResolvers = resolvers.get(host); | ||
|
||
if (hostResolvers) { | ||
hostResolvers.forEach((resolver) => { | ||
if (isError) { | ||
resolver.reject(result as Error); | ||
} else { | ||
resolver.resolve(result as object); | ||
} | ||
}); | ||
|
||
// Clean up resolved promises | ||
resolvers.delete(host); | ||
set({ connectionPromiseResolvers: new Map(resolvers) }); | ||
} | ||
}, | ||
}), | ||
{ | ||
storageKey: 'pendingRequestStore', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import type { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; | ||
|
||
export const getSenderHost = ( | ||
request: ProviderRequestPayload, | ||
): string | null => { | ||
const senderUrl = request.meta?.sender?.url; | ||
if (!senderUrl) return null; | ||
try { | ||
return new URL(senderUrl).hostname; | ||
} catch { | ||
return null; | ||
} | ||
}; | ||
|
||
export const getTabIdString = ( | ||
request: ProviderRequestPayload, | ||
): string | null => { | ||
const tabId = request.meta?.sender?.tab?.id; | ||
if (typeof tabId === 'number' || typeof tabId === 'string') { | ||
return tabId.toString(); | ||
} | ||
return null; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ import { | |
usePendingRequestStore, | ||
} from '~/core/state'; | ||
import { useNetworkStore } from '~/core/state/networks/networks'; | ||
import { getSenderHost, getTabIdString } from '~/core/state/requests/utils'; | ||
import { SessionStorage } from '~/core/storage'; | ||
import { providerRequestTransport } from '~/core/transports'; | ||
import { ProviderRequestPayload } from '~/core/transports/providerRequestTransport'; | ||
|
@@ -66,62 +67,106 @@ const focusOnWindow = (windowId: number) => { | |
const openWindowForTabId = async (tabId: string) => { | ||
const { notificationWindows } = useNotificationWindowStore.getState(); | ||
const notificationWindow = notificationWindows[tabId]; | ||
if (notificationWindow) { | ||
chrome.windows.get( | ||
notificationWindow.id as number, | ||
async (existingWindow) => { | ||
if (chrome.runtime.lastError) { | ||
createNewWindow(tabId); | ||
if (notificationWindow?.id) { | ||
chrome.windows.get(notificationWindow.id, async (existingWindow) => { | ||
if (chrome.runtime.lastError) { | ||
createNewWindow(tabId); | ||
} else { | ||
if (existingWindow.id) { | ||
focusOnWindow(existingWindow.id); | ||
} else { | ||
if (existingWindow) { | ||
focusOnWindow(existingWindow.id as number); | ||
} else { | ||
createNewWindow(tabId); | ||
} | ||
createNewWindow(tabId); | ||
} | ||
}, | ||
); | ||
} | ||
}); | ||
} else { | ||
createNewWindow(tabId); | ||
} | ||
}; | ||
|
||
const handleConnectionResolver = ( | ||
host: string, | ||
addConnectionResolver: ( | ||
host: string, | ||
resolver: { | ||
resolve: (value: object) => void; | ||
reject: (error: Error) => void; | ||
}, | ||
) => void, | ||
): Promise<object> => { | ||
return new Promise((resolve, reject) => { | ||
addConnectionResolver(host, { resolve, reject }); | ||
}); | ||
}; | ||
|
||
const resolveConnectionRequestsIfNeeded = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any duplicative implementation between this concept and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. helped me to prevent some memory leaks, thanks
|
||
request: ProviderRequestPayload, | ||
payload: unknown, | ||
) => { | ||
if (request.method !== 'eth_requestAccounts') return; | ||
const host = getSenderHost(request); | ||
if (!host) return; | ||
const { resolveConnectionRequests } = usePendingRequestStore.getState(); | ||
if (payload) { | ||
resolveConnectionRequests(host, payload); | ||
} else { | ||
resolveConnectionRequests( | ||
host, | ||
new UserRejectedRequestError(Error('User rejected the request.')), | ||
true, | ||
); | ||
} | ||
}; | ||
|
||
/** | ||
* Uses extensionMessenger to send messages to popup for the user to approve or reject | ||
* @param {PendingRequest} request | ||
* @returns {boolean} | ||
* @returns {object} | ||
*/ | ||
const messengerProviderRequest = async ( | ||
messenger: Messenger, | ||
request: ProviderRequestPayload, | ||
) => { | ||
const { addPendingRequest } = usePendingRequestStore.getState(); | ||
// Add pending request to global background state. | ||
addPendingRequest(request); | ||
const { addPendingRequest, addConnectionResolver } = | ||
usePendingRequestStore.getState(); | ||
|
||
// Check if this is a duplicate connection request | ||
const isRequestAdded = addPendingRequest(request); | ||
|
||
let ready = isInitialized(); | ||
while (!ready) { | ||
janek26 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!isRequestAdded) { | ||
// This is a duplicate connection request - add resolver and focus existing window | ||
const host = getSenderHost(request); | ||
const tabId = getTabIdString(request); | ||
if (host && tabId) { | ||
openWindowForTabId(tabId); | ||
return handleConnectionResolver(host, addConnectionResolver); | ||
} | ||
} | ||
|
||
// Wait for initialization | ||
while (!isInitialized()) { | ||
// eslint-disable-next-line no-promise-executor-return, no-await-in-loop | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
ready = isInitialized(); | ||
} | ||
const _hasVault = ready && (await hasVault()); | ||
const _hasVault = await hasVault(); | ||
const passwordSet = _hasVault && (await isPasswordSet()); | ||
|
||
if (_hasVault && passwordSet) { | ||
openWindowForTabId(Number(request.meta?.sender.tab?.id).toString()); | ||
const tabId = getTabIdString(request); | ||
if (_hasVault && passwordSet && tabId) { | ||
openWindowForTabId(tabId); | ||
} else { | ||
goToNewTab({ | ||
url: WELCOME_URL, | ||
}); | ||
goToNewTab({ url: WELCOME_URL }); | ||
} | ||
|
||
// Wait for response from the popup. | ||
const payload: unknown | null = await new Promise((resolve) => | ||
// eslint-disable-next-line no-promise-executor-return | ||
messenger.reply(`message:${request.id}`, async (payload) => | ||
resolve(payload), | ||
), | ||
messenger.reply(`message:${request.id}`, async (payload) => { | ||
resolve(payload); | ||
resolveConnectionRequestsIfNeeded(request, payload); | ||
}), | ||
); | ||
|
||
if (!payload) { | ||
throw new UserRejectedRequestError(Error('User rejected the request.')); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't recall exactly why we adopted this, but this conflicts with a
mockFetch
injection; this could have changed since node v18