Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
83d9a9b
Show loading shimmer while switching popup wallets
KillariDev Sep 7, 2026
e9a588b
Reveal committed wallet selection before background refresh completes
KillariDev Sep 7, 2026
895ca80
Improve popup mode, RPC, and rich switching feedback
KillariDev Sep 7, 2026
712383a
Coordinate popup settings and share snapshot-bound refreshes
KillariDev Sep 7, 2026
672a65e
Recover stalled wallet switches and share popup pending status
KillariDev Sep 7, 2026
4b54c0f
Merge branch 'main' into t3code/wallet-switch-loading-state
KillariDev Sep 7, 2026
5f6b961
Merge branch 'main' into t3code/wallet-switch-loading-state
KillariDev Sep 7, 2026
41cd08a
Clarify active service results and popup request ownership
KillariDev Sep 7, 2026
0fe3ec7
Merge origin/main and preserve current services across popup switches
KillariDev Sep 7, 2026
ac3bb09
Isolate wallet switch replies and centralize settings ownership
KillariDev Sep 7, 2026
55b4072
Extract popup settings controller and publish settings once
KillariDev Sep 7, 2026
55e1a03
Centralize RPC change classification and clarify failure benchmark
KillariDev Sep 7, 2026
caa87fe
Encapsulate wallet switch replies and stop returning service snapshots
KillariDev Sep 7, 2026
019c67d
Merge branch 'main' into t3code/wallet-switch-loading-state
KillariDev Sep 7, 2026
60c37f0
Move RPC change classification out of wire codecs
KillariDev Sep 7, 2026
268749d
Keep coalesced refresh outcomes scoped to each request
KillariDev Sep 7, 2026
8d0d8fb
Keep popup status copy in the UI layer
KillariDev Sep 7, 2026
326cdb4
Clarify refresh queue boundaries and route reset through queue
KillariDev Sep 7, 2026
cc114c4
Guard unsupported simulation and centralize verified switch outcomes
KillariDev Sep 7, 2026
c11997e
Resolve signer display from settings and separate wallet switch owner…
KillariDev Sep 8, 2026
549a51f
Decouple signing switches from simulation and unify RPC entry identity
KillariDev Sep 8, 2026
9069187
Merge origin/main into wallet-switch-loading-state
KillariDev Sep 9, 2026
8a5478b
Restore local RPC preferences and clarify refresh outcomes
KillariDev Sep 9, 2026
e21045e
Read current simulation services after background startup
KillariDev Sep 9, 2026
a53eee1
Merge branch 'main' into t3code/wallet-switch-loading-state
KillariDev Sep 9, 2026
87222a5
Bind wallet deadlines to signer tokens and unify transition services
KillariDev Sep 9, 2026
1feb22d
Document RPC snapshot identity and label tradeoffs
KillariDev Sep 9, 2026
3c614e1
Capture simulation settings explicitly and centralize RPC routing
KillariDev Sep 9, 2026
47eadde
Preserve committed selection follow-ups after provider reset failure
KillariDev Sep 9, 2026
626fa61
Centralize RPC switching and use current simulation services
KillariDev Sep 10, 2026
95b0ef5
Reject dapp network switches while signing as a Safe
KillariDev Sep 10, 2026
7fabdfd
Select request services through the lifecycle owner
KillariDev Sep 10, 2026
c1d03df
Carry service ownership through access and popup lifecycles
KillariDev Sep 10, 2026
0a4f4f1
Keep RPC switching spinner inside the selector
KillariDev Sep 12, 2026
496e420
Document and test signer callback authorization
KillariDev Sep 12, 2026
60589d3
Make popup service snapshot boundaries explicit
KillariDev Sep 12, 2026
33ea74a
Limit simulation failure publication to provider installation
KillariDev Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/css/interceptor-pages.css
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,19 @@ header:has(form[role='search']) h1 {
--btn-brightness: 1;
}

/* Keep live status outside the busy button without adding layout space. */
.dropdown-status-text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

.dropdown-chevron {
margin-left: 0.2em;
display: inline-flex;
Expand Down
18 changes: 12 additions & 6 deletions app/inpage/ts/inpage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ const INTERNAL_BACKGROUND_METHODS = [
const isInternalBackgroundMethod = (method: string) => INTERNAL_BACKGROUND_METHODS.some((internalMethod) => internalMethod === method)

type InterceptedRequestBase = {
readonly walletSwitchRequestId?: string,
readonly interceptorApproved: true,
readonly requestId?: number,
readonly method: string,
Expand Down Expand Up @@ -307,6 +308,7 @@ function chainIdToNetworkVersion(chainId: string) {
}

type InterceptorApprovedMessageCandidate = {
readonly walletSwitchRequestId?: unknown
readonly interceptorApproved?: unknown
readonly method?: unknown
readonly type?: unknown
Expand Down Expand Up @@ -398,6 +400,7 @@ function parseInterceptorApprovedMessage(data: unknown): InterceptedRequestForwa
...(typeof requestId === 'number' ? { requestId } : {}),
...(Array.isArray(params) ? { params } : {}),
...(typeof subscription === 'string' ? { subscription } : {}),
...(typeof data.walletSwitchRequestId === 'string' ? { walletSwitchRequestId: data.walletSwitchRequestId } : {}),
}
if (type === 'forwardToSigner') return {
...base,
Expand Down Expand Up @@ -1414,13 +1417,13 @@ class InterceptorMessageListener {
throw new Error('wrong type')
}

private readonly requestChangeChainFromSigner = async (chainId: string) => {
private readonly requestChangeChainFromSigner = async (chainId: string, walletSwitchRequestId: string) => {
if (this.signerWindowEthereumRequest === undefined) {
await this.sendInternalMessageToBackgroundPage({
method: 'wallet_switchEthereumChain_reply',
params: [{
accept: false,
chainId,
chainId, walletSwitchRequestId,
signerProviderGeneration: this.signerProviderGeneration,
error: { code: METAMASK_ERROR_PROVIDER_DISCONNECTED, message: 'No signer wallet is available to this page. Enable your wallet extension for this site, then try again.' },
}],
Expand All @@ -1431,10 +1434,10 @@ class InterceptorMessageListener {
const outcome = await this.requestFromCurrentSigner({ method: 'wallet_switchEthereumChain', params: [{ chainId }] })
if (outcome.type === 'success') {
const params = outcome.reply === null
? { accept: true as const, chainId, signerProviderGeneration: outcome.signerProviderGeneration }
? { accept: true as const, chainId, walletSwitchRequestId, signerProviderGeneration: outcome.signerProviderGeneration }
: {
accept: false as const,
chainId,
chainId, walletSwitchRequestId,
signerProviderGeneration: outcome.signerProviderGeneration,
error: { code: METAMASK_ERROR_BLANKET_ERROR, message: 'Signer returned an invalid wallet_switchEthereumChain reply.' },
}
Expand All @@ -1446,7 +1449,7 @@ class InterceptorMessageListener {
: this.normalizeSignerErrorForBackground(outcome.error)
await this.sendInternalMessageToBackgroundPage({
method: 'wallet_switchEthereumChain_reply',
params: [{ accept: false, chainId, error, signerProviderGeneration: outcome.signerProviderGeneration }],
params: [{ accept: false, chainId, walletSwitchRequestId, error, signerProviderGeneration: outcome.signerProviderGeneration }],
})
}

Expand Down Expand Up @@ -1607,7 +1610,10 @@ class InterceptorMessageListener {
}
case 'request_signer_to_eth_requestAccounts': return await this.requestAccountsFromSigner()
case 'request_signer_to_eth_accounts': return await this.getAccountsFromSigner()
case 'request_signer_to_wallet_switchEthereumChain': return await this.requestChangeChainFromSigner(replyRequest.result as string)
case 'request_signer_to_wallet_switchEthereumChain': {
if (typeof replyRequest.result !== 'string' || typeof replyRequest.walletSwitchRequestId !== 'string') throw new Error('Invalid wallet switch command')
return await this.requestChangeChainFromSigner(replyRequest.result, replyRequest.walletSwitchRequestId)
}
case 'request_signer_to_wallet_watchAsset': return await this.requestWatchAssetFromSigner(replyRequest.result)
case 'request_signer_connection_status': return await this.connectToSigner(this.signerName)
case 'request_signer_chainId': return await this.requestChainIdFromSigner()
Expand Down
42 changes: 14 additions & 28 deletions app/ts/background/accessManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ import { getUniqueItemsByProperties, replaceElementInReadonlyArray } from '../ut
import { modifyObject } from '../utils/typescript.js'
import type { AddressBookEntries, AddressBookEntry } from '../types/addressBookTypes.js'
import { Semaphore } from '../utils/semaphore.js'
import type { EthereumClientService } from '../simulation/services/EthereumClientService.js'
import type { TokenPriceService } from '../simulation/services/priceEstimator.js'
import type { ResetSimulationServices } from '../simulation/serviceLifecycle.js'
import type { SimulationServicesOwner } from '../simulation/serviceLifecycle.js'
import { mergeStoredWebsiteMetadata } from '../utils/websiteIcons.js'
import { reportUnexpectedError } from '../utils/errors.js'
import { bumpPopupRefreshGeneration } from './popupRefreshGeneration.js'
Expand Down Expand Up @@ -232,12 +230,12 @@ export async function getAssociatedAddresses(settings: Settings, websiteOrigin:
return getUniqueItemsByProperties(all, ['address'])
}

async function askUserForAccessOnConnectionUpdate(ethereum: EthereumClientService, tokenPriceService: TokenPriceService, resetSimulationServices: ResetSimulationServices, websiteTabConnections: WebsiteTabConnections, socket: WebsiteSocket, websiteOrigin: string, activeAddress: AddressBookEntry | undefined, settings: Settings) {
async function askUserForAccessOnConnectionUpdate(simulationServicesOwner: SimulationServicesOwner, websiteTabConnections: WebsiteTabConnections, socket: WebsiteSocket, websiteOrigin: string, activeAddress: AddressBookEntry | undefined, settings: Settings) {
const details = getWebsiteSocketConnection(websiteTabConnections, socket)
if (details === undefined) return

const website = { websiteOrigin, ...await retrieveWebsiteDetails(socket.tabId, websiteOrigin) }
await requestAccessFromUser(ethereum, tokenPriceService, resetSimulationServices, websiteTabConnections, socket, website, undefined, activeAddress, settings, activeAddress, undefined)
await requestAccessFromUser(simulationServicesOwner, websiteTabConnections, socket, website, undefined, activeAddress, settings, activeAddress, undefined)
}

function addIconRefreshTarget(iconRefreshTargets: Map<string, { tabId: number, websiteOrigin: string }>, tabId: number, websiteOrigin: string) {
Expand Down Expand Up @@ -273,7 +271,7 @@ async function updateTabConnections(
return iconRefreshTargets
}

async function promptForWebsiteAccesses(ethereum: EthereumClientService, tokenPriceService: TokenPriceService, resetSimulationServices: ResetSimulationServices, websiteTabConnections: WebsiteTabConnections, throwOnError = false) {
async function promptForWebsiteAccesses(simulationServicesOwner: SimulationServicesOwner, websiteTabConnections: WebsiteTabConnections, throwOnError = false) {
for (const tabConnection of websiteTabConnections.values()) {
for (const connection of Object.values(tabConnection.connections)) {
if (!connection.wantsToConnect) continue
Expand All @@ -282,7 +280,7 @@ async function promptForWebsiteAccesses(ethereum: EthereumClientService, tokenPr
const settings = await getSettings()
const { activeAddress, access } = await getConnectionAccess(websiteTabConnections, connection, settings)
if (access !== 'askAccess') continue
await askUserForAccessOnConnectionUpdate(ethereum, tokenPriceService, resetSimulationServices, websiteTabConnections, connection.socket, connection.websiteOrigin, activeAddress, settings)
await askUserForAccessOnConnectionUpdate(simulationServicesOwner, websiteTabConnections, connection.socket, connection.websiteOrigin, activeAddress, settings)
} catch (error) {
if (throwOnError) throw error
await reportUnexpectedError(error)
Expand Down Expand Up @@ -423,18 +421,16 @@ export async function reconcileWebsiteApprovalAccesses(

// Call after releasing the settings lock: access dialogs can activate another address.
export async function finishWebsiteAccessUpdate(
ethereum: EthereumClientService | undefined,
tokenPriceService: TokenPriceService | undefined,
resetSimulationServices: ResetSimulationServices | undefined,
simulationServicesOwner: SimulationServicesOwner | undefined,
websiteTabConnections: WebsiteTabConnections,
update: WebsiteAccessUpdate,
promptForAccessesIfNeeded: boolean,
throwOnError = false,
) {
const iconRefreshTargets = new Map<string, { tabId: number, websiteOrigin: string }>()
for (const target of update.iconRefreshTargets) addIconRefreshTarget(iconRefreshTargets, target.tabId, target.websiteOrigin)
if (promptForAccessesIfNeeded && ethereum !== undefined && tokenPriceService !== undefined && resetSimulationServices !== undefined) {
await promptForWebsiteAccesses(ethereum, tokenPriceService, resetSimulationServices, websiteTabConnections, throwOnError)
if (promptForAccessesIfNeeded && simulationServicesOwner !== undefined) {
await promptForWebsiteAccesses(simulationServicesOwner, websiteTabConnections, throwOnError)
}
try {
for (const tabState of await getAllTabStates()) {
Expand All @@ -452,31 +448,25 @@ export async function finishWebsiteAccessUpdate(
}

export async function updateWebsiteApprovalAccesses(
ethereum: EthereumClientService | undefined,
tokenPriceService: TokenPriceService | undefined,
resetSimulationServices: ResetSimulationServices | undefined,
simulationServicesOwner: SimulationServicesOwner | undefined,
websiteTabConnections: WebsiteTabConnections,
settings: Settings,
promptForAccessesIfNeeded: boolean,
throwOnError = false,
): Promise<number> {
const update = await reconcileWebsiteApprovalAccesses(websiteTabConnections, settings, throwOnError)
await finishWebsiteAccessUpdate(ethereum, tokenPriceService, resetSimulationServices, websiteTabConnections, update, promptForAccessesIfNeeded, throwOnError)
await finishWebsiteAccessUpdate(simulationServicesOwner, websiteTabConnections, update, promptForAccessesIfNeeded, throwOnError)
return update.popupRefreshGeneration
}

export async function finalizeWebsiteAccessChange(
ethereum: EthereumClientService | undefined,
tokenPriceService: TokenPriceService | undefined,
resetSimulationServices: ResetSimulationServices | undefined,
simulationServicesOwner: SimulationServicesOwner | undefined,
websiteTabConnections: WebsiteTabConnections,
settings: Settings,
promptForAccessesIfNeeded: boolean,
): Promise<Settings> {
await updateWebsiteApprovalAccesses(
ethereum,
tokenPriceService,
resetSimulationServices,
simulationServicesOwner,
websiteTabConnections,
settings,
promptForAccessesIfNeeded,
Expand All @@ -486,9 +476,7 @@ export async function finalizeWebsiteAccessChange(
}

export async function persistWebsiteAccessChange(
ethereum: EthereumClientService | undefined,
tokenPriceService: TokenPriceService | undefined,
resetSimulationServices: ResetSimulationServices | undefined,
simulationServicesOwner: SimulationServicesOwner | undefined,
websiteTabConnections: WebsiteTabConnections,
website: Website,
access: boolean,
Expand All @@ -497,9 +485,7 @@ export async function persistWebsiteAccessChange(
): Promise<Settings> {
await setAccess(website, access, address)
return await finalizeWebsiteAccessChange(
ethereum,
tokenPriceService,
resetSimulationServices,
simulationServicesOwner,
websiteTabConnections,
await getSettings(),
promptForAccessesIfNeeded,
Expand Down
Loading
Loading