diff --git a/chrome-extension/src/background/chains/bitcoinCashHandler.ts b/chrome-extension/src/background/chains/bitcoinCashHandler.ts index 6918d25..c5a3a0b 100644 --- a/chrome-extension/src/background/chains/bitcoinCashHandler.ts +++ b/chrome-extension/src/background/chains/bitcoinCashHandler.ts @@ -55,7 +55,7 @@ export const handleBitcoinCashRequest = async ( chrome.runtime.sendMessage({ action: 'utxo_build_tx', unsignedTx: requestInfo }); } catch (e) { console.error(e); - chrome.runtime.sendMessage({ action: 'transaction_error', error: JSON.stringify(e) }); + chrome.runtime.sendMessage({ action: 'transaction_error', eventId: requestInfo.id, error: JSON.stringify(e) }); } }; buildTx(); @@ -104,6 +104,7 @@ export const handleBitcoinCashRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://blockchair.com/bitcoin-cash/transaction/', }); diff --git a/chrome-extension/src/background/chains/bitcoinHandler.ts b/chrome-extension/src/background/chains/bitcoinHandler.ts index 765e696..302b569 100644 --- a/chrome-extension/src/background/chains/bitcoinHandler.ts +++ b/chrome-extension/src/background/chains/bitcoinHandler.ts @@ -85,6 +85,7 @@ export const handleBitcoinRequest = async ( console.error(e); chrome.runtime.sendMessage({ action: 'transaction_error', + eventId: requestInfo.id, error: JSON.stringify(e), }); } @@ -145,6 +146,7 @@ export const handleBitcoinRequest = async ( chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash: txHash, explorerTxLink: 'https://mempool.space/tx/', }); @@ -153,6 +155,7 @@ export const handleBitcoinRequest = async ( console.error(tag, e); chrome.runtime.sendMessage({ action: 'transaction_error', + eventId: requestInfo.id, error: JSON.stringify(e), }); } diff --git a/chrome-extension/src/background/chains/cosmosHandler.ts b/chrome-extension/src/background/chains/cosmosHandler.ts index 93419ca..84663c1 100644 --- a/chrome-extension/src/background/chains/cosmosHandler.ts +++ b/chrome-extension/src/background/chains/cosmosHandler.ts @@ -103,6 +103,7 @@ export const handleCosmosRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://www.mintscan.io/cosmos/tx/', }); diff --git a/chrome-extension/src/background/chains/dashHandler.ts b/chrome-extension/src/background/chains/dashHandler.ts index e7db454..798d4e9 100644 --- a/chrome-extension/src/background/chains/dashHandler.ts +++ b/chrome-extension/src/background/chains/dashHandler.ts @@ -55,7 +55,7 @@ export const handleDashRequest = async ( chrome.runtime.sendMessage({ action: 'utxo_build_tx', unsignedTx: requestInfo }); } catch (e) { console.error(e); - chrome.runtime.sendMessage({ action: 'transaction_error', error: JSON.stringify(e) }); + chrome.runtime.sendMessage({ action: 'transaction_error', eventId: requestInfo.id, error: JSON.stringify(e) }); } }; buildTx(); @@ -104,6 +104,7 @@ export const handleDashRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://blockchair.com/dash/transaction/', }); diff --git a/chrome-extension/src/background/chains/dogecoinHandler.ts b/chrome-extension/src/background/chains/dogecoinHandler.ts index 01a7b79..d3306ed 100644 --- a/chrome-extension/src/background/chains/dogecoinHandler.ts +++ b/chrome-extension/src/background/chains/dogecoinHandler.ts @@ -58,7 +58,7 @@ export const handleDogecoinRequest = async ( chrome.runtime.sendMessage({ action: 'utxo_build_tx', unsignedTx: requestInfo }); } catch (e) { console.error(e); - chrome.runtime.sendMessage({ action: 'transaction_error', error: JSON.stringify(e) }); + chrome.runtime.sendMessage({ action: 'transaction_error', eventId: requestInfo.id, error: JSON.stringify(e) }); } }; buildTx(); @@ -107,6 +107,7 @@ export const handleDogecoinRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://blockchair.com/dogecoin/transaction/', }); diff --git a/chrome-extension/src/background/chains/ethereumHandler.ts b/chrome-extension/src/background/chains/ethereumHandler.ts index 36fe472..4dcdb06 100644 --- a/chrome-extension/src/background/chains/ethereumHandler.ts +++ b/chrome-extension/src/background/chains/ethereumHandler.ts @@ -74,46 +74,6 @@ type Event = { timestamp: string; }; -let isPopupOpen = false; // Flag to track popup state - -const openPopup = function () { - const tag = TAG + ' | openPopup | '; - try { - console.log(tag, 'Opening popup'); - chrome.windows.create( - { - url: chrome.runtime.getURL('popup/index.html'), // Adjust the URL to your popup file - type: 'popup', - width: 400, - height: 600, - }, - window => { - if (chrome.runtime.lastError) { - console.error('Error creating popup:', chrome.runtime.lastError); - isPopupOpen = false; - } else { - console.log('Popup window created:', window); - - // Optionally, handle the popup window focus or other behaviors - } - }, - ); - } catch (e) { - console.error(tag, e); - } -}; - -const requireUnlock = async function () { - const tag = TAG + ' | requireUnlock | '; - try { - console.log(tag, 'requireUnlock for domain'); - openPopup(); - } catch (e) { - console.error(e); - isPopupOpen = false; - } -}; - const convertHexToDecimalChainId = (hexChainId: string): number => { return parseInt(hexChainId, 16); }; @@ -757,6 +717,7 @@ const handleTransfer = async (params, requestInfo, ADDRESS, KEEPKEY_WALLET, requ // Notify transaction completion chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash: txid, explorerTxLink: currentProviderCtx?.explorerTxLink, networkId, @@ -890,10 +851,10 @@ const processApprovedEvent = async (method: string, params: any, KEEPKEY_WALLET: case 'personal_sign': // EIP-191 personal_sign: params = [message, address]. Prefer the dApp-supplied // address so multi-account wallets sign with the correct derivation path. - result = await signMessage(params[0], KEEPKEY_WALLET, params[1] || ADDRESS); + result = await signMessage(params[0], KEEPKEY_WALLET, params[1] || ADDRESS, id); break; case 'eth_sign': - result = await signMessage(params[1], KEEPKEY_WALLET, params[0]); + result = await signMessage(params[1], KEEPKEY_WALLET, params[0], id); break; case 'eth_sendTransaction': result = await sendTransaction(params, KEEPKEY_WALLET, ADDRESS, id); @@ -901,7 +862,7 @@ const processApprovedEvent = async (method: string, params: any, KEEPKEY_WALLET: case 'eth_signTypedData': case 'eth_signTypedData_v3': case 'eth_signTypedData_v4': - result = await signTypedData(params, KEEPKEY_WALLET, ADDRESS); + result = await signTypedData(params, KEEPKEY_WALLET, ADDRESS, id); break; case 'eth_signTransaction': result = await signTransaction(params[0], KEEPKEY_WALLET); @@ -919,7 +880,7 @@ const processApprovedEvent = async (method: string, params: any, KEEPKEY_WALLET: } }; -const signMessage = async (message, KEEPKEY_WALLET, ADDRESS: string) => { +const signMessage = async (message, KEEPKEY_WALLET, ADDRESS: string, eventId?: string) => { const tag = TAG + ' [signMessage] '; try { console.log(tag, '**** message: ', message); @@ -947,6 +908,7 @@ const signMessage = async (message, KEEPKEY_WALLET, ADDRESS: string) => { // Notify popup that signature is complete chrome.runtime.sendMessage({ action: 'signature_complete', + eventId, signature: signatureHex, }); @@ -1091,7 +1053,7 @@ const signTransaction = async (transaction: any, KEEPKEY_WALLET: any) => { } }; -const signTypedData = async (params: any, KEEPKEY_WALLET: any, ADDRESS: string) => { +const signTypedData = async (params: any, KEEPKEY_WALLET: any, ADDRESS: string, eventId?: string) => { const tag = ' | signTypedData | '; try { console.log(tag, '**** params: ', params); @@ -1116,6 +1078,7 @@ const signTypedData = async (params: any, KEEPKEY_WALLET: any, ADDRESS: string) // Notify popup that signature is complete chrome.runtime.sendMessage({ action: 'signature_complete', + eventId, signature: signatureHex, }); @@ -1204,6 +1167,7 @@ const sendTransaction = async (params: any, KEEPKEY_WALLET: any, ADDRESS: string //push event chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: id, txHash: txHash, explorerTxLink: currentProvider.explorerTxLink, networkId: currentProvider.networkId, diff --git a/chrome-extension/src/background/chains/litecoinHandler.ts b/chrome-extension/src/background/chains/litecoinHandler.ts index fa56338..bd6515d 100644 --- a/chrome-extension/src/background/chains/litecoinHandler.ts +++ b/chrome-extension/src/background/chains/litecoinHandler.ts @@ -55,7 +55,7 @@ export const handleLitecoinRequest = async ( chrome.runtime.sendMessage({ action: 'utxo_build_tx', unsignedTx: requestInfo }); } catch (e) { console.error(e); - chrome.runtime.sendMessage({ action: 'transaction_error', error: JSON.stringify(e) }); + chrome.runtime.sendMessage({ action: 'transaction_error', eventId: requestInfo.id, error: JSON.stringify(e) }); } }; buildTx(); @@ -104,6 +104,7 @@ export const handleLitecoinRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://blockchair.com/litecoin/transaction/', }); diff --git a/chrome-extension/src/background/chains/mayaHandler.ts b/chrome-extension/src/background/chains/mayaHandler.ts index 28ce90d..5b6af87 100644 --- a/chrome-extension/src/background/chains/mayaHandler.ts +++ b/chrome-extension/src/background/chains/mayaHandler.ts @@ -103,6 +103,7 @@ export const handleMayaRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://www.mayascan.org/tx/', }); diff --git a/chrome-extension/src/background/chains/osmosisHandler.ts b/chrome-extension/src/background/chains/osmosisHandler.ts index 339c31c..36ea0ef 100644 --- a/chrome-extension/src/background/chains/osmosisHandler.ts +++ b/chrome-extension/src/background/chains/osmosisHandler.ts @@ -103,6 +103,7 @@ export const handleOsmosisRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://www.mintscan.io/osmosis/tx/', }); diff --git a/chrome-extension/src/background/chains/rippleHandler.ts b/chrome-extension/src/background/chains/rippleHandler.ts index cb18769..d9c81ba 100644 --- a/chrome-extension/src/background/chains/rippleHandler.ts +++ b/chrome-extension/src/background/chains/rippleHandler.ts @@ -103,6 +103,7 @@ export const handleRippleRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://xrpscan.com/tx/', }); diff --git a/chrome-extension/src/background/chains/solanaHandler.ts b/chrome-extension/src/background/chains/solanaHandler.ts index cc36b93..0a3327d 100644 --- a/chrome-extension/src/background/chains/solanaHandler.ts +++ b/chrome-extension/src/background/chains/solanaHandler.ts @@ -132,8 +132,11 @@ async function getSolanaAddress(): Promise { /** Build the event object for popup approval flow */ function buildEvent(requestInfo: any, method: string, params: any[]) { + // Ensure requestInfo.id is set so callers downstream (including message payloads + // that tag chrome.runtime events with eventId) reference the same id we store. + if (!requestInfo.id) requestInfo.id = uuidv4(); return { - id: requestInfo.id || uuidv4(), + id: requestInfo.id, networkId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', chain: 'solana', href: requestInfo.href, @@ -373,7 +376,7 @@ export const handleSolanaRequest = async ( const messageBase64 = toBase64(messageArray); const signatureArray = await signMessageViaRest(messageBase64); - chrome.runtime.sendMessage({ action: 'signature_complete' }).catch(() => {}); + chrome.runtime.sendMessage({ action: 'signature_complete', eventId: requestInfo.id }).catch(() => {}); return signatureArray; } @@ -393,7 +396,7 @@ export const handleSolanaRequest = async ( // Return the fully signed transaction (vault replaces dummy sig at bytes 1-64) const signedTxArray = fromBase64(txSignResult.serializedTx); - chrome.runtime.sendMessage({ action: 'signature_complete' }).catch(() => {}); + chrome.runtime.sendMessage({ action: 'signature_complete', eventId: requestInfo.id }).catch(() => {}); return signedTxArray; } @@ -417,6 +420,7 @@ export const handleSolanaRequest = async ( chrome.runtime .sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash: txSignature, explorerTxLink: 'https://solscan.io/tx/', networkId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', diff --git a/chrome-extension/src/background/chains/thorchainHandler.ts b/chrome-extension/src/background/chains/thorchainHandler.ts index c676f20..54c5aa5 100644 --- a/chrome-extension/src/background/chains/thorchainHandler.ts +++ b/chrome-extension/src/background/chains/thorchainHandler.ts @@ -105,6 +105,7 @@ export const handleThorchainRequest = async ( await requestStorage.updateEventById(requestInfo.id, response); chrome.runtime.sendMessage({ action: 'transaction_complete', + eventId: requestInfo.id, txHash, explorerTxLink: 'https://runescan.io/tx/', }); diff --git a/chrome-extension/src/background/methods.ts b/chrome-extension/src/background/methods.ts index 9150c24..5266db2 100644 --- a/chrome-extension/src/background/methods.ts +++ b/chrome-extension/src/background/methods.ts @@ -18,58 +18,79 @@ import { createProviderRpcError, ProviderRpcError, formatUserError } from './uti const TAG = ' | METHODS | '; -let isPopupOpen = false; // Flag to track popup state -let popupWindowId: number | null = null; // Track the popup window ID +const POPUP_URL = chrome.runtime.getURL('popup/index.html'); -const openPopup = function () { +// Single window-close listener registered at module load, not per-open. +// Listeners watching for "this popup closed" subscribe via onPopupClosed(). +const popupCloseListeners = new Set<(windowId: number) => void>(); +const onPopupClosed = (listener: (windowId: number) => void): (() => void) => { + popupCloseListeners.add(listener); + return () => { + popupCloseListeners.delete(listener); + }; +}; +chrome.windows.onRemoved.addListener(windowId => { + popupCloseListeners.forEach(fn => { + try { + fn(windowId); + } catch (e) { + console.error(TAG, 'popup close listener threw', e); + } + }); +}); + +// Track the current approval popup's windowId. Source of truth is +// chrome.windows.getAll(), not this variable — the variable is an optimistic +// cache that can be stale after a service worker restart. +let popupWindowId: number | null = null; + +// Find an already-open approval popup by URL. Survives service worker restarts. +const findExistingPopup = async (): Promise => { + try { + const windows = await chrome.windows.getAll({ populate: true, windowTypes: ['popup'] }); + for (const w of windows) { + const tabs = w.tabs || []; + if (tabs.some(t => t.url === POPUP_URL || t.pendingUrl === POPUP_URL)) { + return w; + } + } + } catch (e) { + console.error(TAG, 'findExistingPopup failed', e); + } + return null; +}; + +// Returns the popup windowId once the popup is guaranteed to exist. Focuses +// an existing popup if one is already open; otherwise creates a new one. +const openPopup = async (): Promise => { const tag = TAG + ' | openPopup | '; try { - // If popup is already open, focus it instead of creating a new one - if (isPopupOpen && popupWindowId !== null) { + const existing = await findExistingPopup(); + if (existing?.id != null) { + popupWindowId = existing.id; console.log(tag, 'Popup already open, focusing existing window:', popupWindowId); - chrome.windows.update(popupWindowId, { focused: true }).catch(err => { - console.error(tag, 'Failed to focus existing popup, creating new one:', err); - isPopupOpen = false; - popupWindowId = null; - openPopup(); - }); - return; + try { + await chrome.windows.update(popupWindowId, { focused: true }); + } catch (e) { + console.warn(tag, 'Failed to focus existing popup', e); + } + return popupWindowId; } console.log(tag, 'Opening popup'); - isPopupOpen = true; - chrome.windows.create( - { - url: chrome.runtime.getURL('popup/index.html'), // Adjust the URL to your popup file - type: 'popup', - width: 360, - height: 900, - }, - window => { - if (chrome.runtime.lastError) { - console.error('Error creating popup:', chrome.runtime.lastError); - isPopupOpen = false; - popupWindowId = null; - } else { - console.log('Popup window created:', window); - popupWindowId = window?.id || null; - - // Listen for when the popup is closed - chrome.windows.onRemoved.addListener(function windowClosedListener(windowId) { - if (windowId === popupWindowId) { - console.log(tag, 'Popup closed, resetting state'); - isPopupOpen = false; - popupWindowId = null; - chrome.windows.onRemoved.removeListener(windowClosedListener); - } - }); - } - }, - ); + const created = await chrome.windows.create({ + url: POPUP_URL, + type: 'popup', + width: 360, + height: 900, + }); + popupWindowId = created?.id ?? null; + console.log(tag, 'Popup window created:', popupWindowId); + return popupWindowId; } catch (e) { console.error(tag, e); - isPopupOpen = false; popupWindowId = null; + return null; } }; @@ -153,22 +174,41 @@ const requireApproval = async function ( // throw new Error('Event not saved'); // } - openPopup(); + const activePopupId = await openPopup(); - // Wait for user's decision and return the result + // Wait for user's decision and return the result. Cleans up on ANY of: + // - user approves/rejects in popup (eth_sign_response arrives) + // - popup window is closed (treated as implicit reject) return new Promise(resolve => { - const listener = (message: any, sender: chrome.runtime.MessageSender, sendResponse: any) => { - if (message.action === 'eth_sign_response' && message.response.eventId === requestInfo.id) { + let settled = false; + let unsubClose: (() => void) | null = null; + + const cleanup = () => { + chrome.runtime.onMessage.removeListener(listener); + if (unsubClose) unsubClose(); + }; + + const listener = (message: any) => { + if (message?.action === 'eth_sign_response' && message?.response?.eventId === requestInfo.id) { + if (settled) return; + settled = true; console.log(tag, 'Received eth_sign_response for event:', message.response.eventId); - chrome.runtime.onMessage.removeListener(listener); - if (message.response.decision === 'accept') { - resolve({ success: true }); - } else { - resolve({ success: false }); - } + cleanup(); + resolve({ success: message.response.decision === 'accept' }); } }; chrome.runtime.onMessage.addListener(listener); + + unsubClose = onPopupClosed((closedId: number) => { + // If we couldn't determine the popup windowId at open time, treat any + // popup close as potentially ours — safer than hanging forever. + if (activePopupId != null && closedId !== activePopupId) return; + if (settled) return; + settled = true; + console.log(tag, 'Popup closed without response, rejecting approval for event:', requestInfo.id); + cleanup(); + resolve({ success: false }); + }); }); } catch (e) { console.error(tag, e); @@ -282,6 +322,7 @@ export const handleWalletRequest = async ( //push error to the popup chrome.runtime.sendMessage({ action: 'transaction_error', + eventId: requestInfo?.id, error: errorMessage, }); diff --git a/pages/popup/src/components/Transaction.tsx b/pages/popup/src/components/Transaction.tsx index 40af830..8cf6512 100644 --- a/pages/popup/src/components/Transaction.tsx +++ b/pages/popup/src/components/Transaction.tsx @@ -91,6 +91,10 @@ const Transaction = ({ event, reloadEvents }: { event: any; reloadEvents: () => useEffect(() => { const handleMessage = (message: any) => { console.log('message received:', message); + // Only handle messages addressed to THIS event. Messages without an + // eventId are legacy/unscoped; accept them for backward compatibility + // so nothing hangs if an older handler is still in flight. + if (message?.eventId && message.eventId !== event.id) return; if (message.action === 'transaction_complete') { // Play success sound after device signs transaction try {