How the Nautilus Wallet Extension Popup Communicates with the Background Script
Nautilus Wallet uses the webext-bridge library to establish typed message passing between the Vue-based popup UI and the service worker background script, coordinating requests through a shared AsyncRequestQueue.
The nautls/nautilus-wallet repository implements a sophisticated communication layer that allows the browser extension popup to securely exchange data with the persistent background script. Understanding how the extension popup communicates with the background script is essential for developers building on or contributing to this Ergo blockchain wallet.
The Architecture: webext-bridge and Typed Message Passing
Nautilus Wallet abstracts Chrome's native runtime.sendMessage API using the webext-bridge library. This creates a type-safe messaging layer that distinguishes between the popup context ("popup") and the background context ("background").
The architecture relies on three core components:
src/extension/connector/rpc/uiRpcHandlers.ts– Registers popup-side message handlers and sends initialization signalssrc/extension/background/background.ts– Listens for messages, manages the request queue, and opens the popup when user interaction is requiredsrc/extension/connector/rpc/asyncRequestQueue.ts– Shared queue that correlates pending requests with their eventual UI-driven responses
Step-by-Step Communication Flow
1. Popup Initialization and the Loaded Event
When the Vue popup application mounts in src/extension/popup/main.ts, it triggers the RPC handler registration. The popup immediately notifies the background script that it is ready to receive requests:
// src/extension/connector/rpc/uiRpcHandlers.ts
import { sendMessage, onMessage } from "webext-bridge/popup";
export function registerRpcHooks() {
// Signal that the popup UI is loaded and ready
sendMessage(InternalEvent.Loaded, _, BACKGROUND);
// Register handlers for specific request types
onMessage(InternalRequest.Connect, ({ data }) =>
handle(InternalRequest.Connect, data)
);
}
This InternalEvent.Loaded message is critical because it allows the background script to flush any pending requests that were queued while the popup was closed.
2. Background Script Message Handling
The background script in src/extension/background/background.ts sets up persistent listeners using onMessage from webext-bridge/background. It distinguishes between internal endpoints and external dApp connections:
// src/extension/background/background.ts
import { onMessage, sendMessage } from "webext-bridge/background";
onMessage(InternalRequest.Connect, async ({ data, sender }) => {
if (!isInternalEndpoint(sender)) return false;
// Check if already authorized
const authorized = await checkConnection(data.payload.origin);
if (authorized) return true;
// Requires user interaction - open popup
return await openWindow(InternalRequest.Connect, data, sender.tabId);
});
For requests requiring authentication, the background uses onMessageAuth, which automatically validates the session before executing the handler.
3. Queuing Requests and Opening the Popup
When user interaction is required, the background script creates a promise-based request and pushes it onto the AsyncRequestQueue. It then opens the popup window and returns the pending promise:
// src/extension/background/background.ts
async function openWindow<T extends InternalRequest>(
request: T,
data: DataWithPayload,
tabId?: number
): Promise<GetReturnType<T>> {
// Create promise and enqueue
const promise = requests.push<GetReturnType<T>>({
type: request,
origin: data.payload.origin,
favicon: data.payload.favicon,
data
});
// Open the popup UI
await createWindow(tabId);
// Return promise that resolves when user completes interaction
return promise;
}
The AsyncRequestQueue stores the resolve and reject functions alongside the request metadata, allowing the background to fulfill the promise later when the popup sends back the result.
4. Resolving Requests from Background to Popup
Once the popup opens and sends the InternalEvent.Loaded signal, the background flushes the queue. For each pending request, it sends the request data to the popup using sendMessage, then resolves the original promise with the popup's response:
// src/extension/background/background.ts
onMessage(InternalEvent.Loaded, async ({ sender }) => {
if (!isInternalEndpoint(sender)) return;
let request: AsyncRequest | undefined;
// Process all queued requests
do {
request = requests.pop();
if (!request) continue;
const payload = {
origin: request.origin,
favicon: request.favicon
};
const data = request.data ? { payload, ...request.data } : { payload };
// Send to popup and wait for user decision
const result = await sendMessage(request.type, data, "popup");
request.resolve(result);
} while (request);
});
On the popup side, the handle function in uiRpcHandlers.ts receives this message, navigates to the appropriate view (e.g., the Connect or Sign Transaction screen), and returns a promise that resolves when the user clicks approve or reject.
Key Implementation Files
Understanding the file structure helps navigate the communication logic:
src/extension/background/background.ts– Service worker entry point; registers message listeners, manages theAsyncRequestQueue, and opens the popup window when user interaction is required.src/extension/connector/rpc/uiRpcHandlers.ts– Popup-side RPC handlers; sends theLoadedevent and processes incoming requests from the background.src/extension/connector/rpc/asyncRequestQueue.ts– Shared data structure that stores pending requests with their resolve/reject callbacks.src/extension/popup/main.ts– Vue application entry point that initializes the popup UI and triggers RPC registration.src/common/browser.ts– Utility functions for browser context detection and endpoint validation.
Summary
- Nautilus Wallet uses
webext-bridgeto abstract Chrome's native messaging API between the popup and background script. - The
InternalEvent.Loadedsignal synchronizes the popup UI with queued background requests. - An
AsyncRequestQueuecorrelates pending promises with user interactions, allowing the background to await popup responses. - The background script opens the popup via
createWindowwhen authentication or transaction signing requires user approval. - All messages are typed using
InternalRequestandInternalEventenums to ensure compile-time safety.
Frequently Asked Questions
What library does Nautilus Wallet use for popup-to-background communication?
Nautilus Wallet uses the webext-bridge library, which wraps Chrome's runtime.sendMessage API to provide type-safe, promise-based messaging between extension contexts. This library allows the popup and background script to communicate using sendMessage and onMessage functions imported from webext-bridge/popup and webext-bridge/background respectively.
How does the background script know when the popup is ready to receive requests?
The background script listens for the InternalEvent.Loaded message, which the popup sends immediately upon initialization in src/extension/connector/rpc/uiRpcHandlers.ts. When this event is received in src/extension/background/background.ts, the background flushes the AsyncRequestQueue, sending any pending requests to the now-ready popup UI.
What is the purpose of the AsyncRequestQueue in Nautilus Wallet?
The AsyncRequestQueue is a shared data structure that stores pending requests along with their resolve and reject callbacks. It allows the background script to pause execution while waiting for user interaction in the popup, then resume by resolving the promise when the popup returns a result. This queue is defined in src/extension/connector/rpc/asyncRequestQueue.ts and used by both the popup and background contexts.
Can the popup send messages to the background without opening a new window?
Yes, the popup can send messages to the background at any time using sendMessage from webext-bridge/popup, regardless of whether it was opened by the background or the user clicked the extension icon. However, for security-critical operations like signing transactions or connecting dApps, the background typically initiates the flow by opening the popup to ensure the user sees the approval UI.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →