How the dApp RPC Protocol Facilitates Communication with Nautilus Wallet
Nautilus Wallet exposes a typed dApp RPC protocol built on the webext-bridge library, creating secure asynchronous JSON-RPC channels between injected page scripts, background workers, and UI components to process blockchain requests.
The dApp RPC protocol implemented in the nautls/nautilus-wallet repository enables decentralized applications to interact with wallet functionality through a structured, type-safe messaging system. This architecture isolates external web pages from sensitive private keys while maintaining bidirectional communication flow across the browser extension's distinct execution contexts.
Architecture Overview
The protocol operates across three isolated execution environments that collectively handle requests from web pages to the wallet's internal ledger.
The Three-Layer Communication Stack
- Content-script side – Injects the
ergoConnectorandergoobjects into the page context, serving as the dApp's entry point for blockchain operations. - Background (UI) side – Receives RPC calls from the content script, forwards them to the wallet UI, and manages response routing.
- UI components – Vue.js components (such as
BalanceView.vue) that execute wallet operations including UTXO fetching and transaction signing.
Namespace Isolation
All messages are scoped with a unique UUID defined as RPC_NAMESPACE in src/extension/connector/rpc/protocol.ts. This namespace isolation prevents message collisions when multiple wallet extensions are installed in the same browser instance.
Core Protocol Components
The dApp RPC protocol relies on several TypeScript-defined structures that enforce type safety and ordered execution.
External vs. Internal Request Types
In src/extension/connector/rpc/protocol.ts, the protocol distinguishes between two request origins:
- ExternalRequest – Declares operations originating from the web page (dApp), such as
GetBalanceorSignTx. - InternalRequest – Declares operations originating from the wallet UI components.
Both request types share the same payload shape through the WithPayload utility type, ensuring consistent data structures across the bridge.
Type Safety with ProtocolMap
The ProtocolMap interface in src/types/d.ts/webext-rpc.d.ts links each request enum member to its specific request/response type pair. This TypeScript-only construct enables webext-bridge to generate fully type-safe RPC calls, preventing payload mismatches at compile time.
Request Serialization via AsyncRequestQueue
The AsyncRequestQueue class in src/extension/connector/rpc/asyncRequestQueue.ts guarantees ordered, single-threaded processing of UI-side requests. When the background script receives an InternalRequest, it enqueues an AsyncRequest object and resolves the associated promise only when the UI component explicitly signals completion through success() or error() callbacks.
Message Flow and Execution
The dApp RPC protocol follows a strict four-step pipeline to process blockchain requests securely.
Step 1: dApp to Content Script
Web applications interact with the injected API:
// Example dApp code
await ergoConnector.nautilus.connect();
const utxos = await ergo.get_utxos({ target: "default" });
const signedTx = await ergo.sign_tx({ transaction: unsignedTx });
Each method call maps to an ExternalRequest enum value defined in protocol.ts and transmits via webext-bridge.
Step 2: Content Script to Background
The bridge routes the message to the background script, which converts the ExternalRequest into an InternalRequest with identical payload structure.
Step 3: Background to UI Components
The background script enqueues the request via queue.enqueue() exposed in src/extension/connector/rpc/uiRpcHandlers.ts. UI components consume these requests through event listeners and execute wallet operations against the internal ledger.
Step 4: Response to dApp
UI components resolve the queued request using the success(data) or error(err) helpers from protocol.ts. The result traverses back through the bridge layers, ultimately resolving the original promise in the web page's execution context.
Implementation Examples
Registering RPC Hooks
The background script initializes the protocol in src/extension/connector/rpc/uiRpcHandlers.ts:
export const queue = new AsyncRequestQueue();
export function registerRpcHooks() {
bridge.on(ExternalRequest.GetBalance, async (payload) => {
const result = await queue.enqueue({
type: InternalRequest.GetBalance,
payload
});
return result;
});
}
The registerRpcHooks() function is invoked during extension startup from src/extension/connector/main.ts, wiring the entire communication system.
UI Component Request Handling
Vue components interact with the queue to process operations:
// src/extension/connector/views/BalanceView.vue
import { queue } from "@/extension/connector/rpc/uiRpcHandlers";
queue.on(InternalRequest.GetBalance, async (msg) => {
const balances = await ledger.getBalances();
return success(balances);
});
This pattern ensures that sensitive wallet operations execute only within the trusted UI context while maintaining async communication with the untrusted page environment.
Summary
- The dApp RPC protocol uses
webext-bridgeto establish typed JSON-RPC channels across browser extension contexts. - Namespace isolation via
RPC_NAMESPACEprevents cross-extension interference. - Dual request types (
ExternalRequestandInternalRequest) inprotocol.tsmaintain security boundaries while sharing payload structures. - AsyncRequestQueue serializes UI-side processing to prevent race conditions during user approvals.
- The entry point in
src/extension/connector/main.tsbootstraps the entire system by callingregisterRpcHooks().
Frequently Asked Questions
What library powers the dApp RPC protocol in Nautilus Wallet?
The protocol builds upon the webext-bridge library from Google Chrome Labs, which provides the underlying message transport mechanism between content scripts, background workers, and extension pages. This library handles serialization, routing, and promise-based async communication while the Nautilus-specific code in protocol.ts defines the typed request/response contracts.
How does Nautilus Wallet prevent cross-extension message collisions?
All RPC messages are prefixed with a unique namespace UUID (RPC_NAMESPACE) defined in src/extension/connector/rpc/protocol.ts. This namespace scoping ensures that messages intended for Nautilus Wallet are never intercepted or processed by other browser extensions that might also use webext-bridge, preventing both security vulnerabilities and logical errors.
What is the difference between ExternalRequest and InternalRequest?
ExternalRequest enumerates operations that originate from untrusted web pages (dApps) calling the injected ergo object, while InternalRequest enumerates the same operations when forwarded to the trusted UI context. The background script in uiRpcHandlers.ts acts as a translator, converting external calls into internal ones while preserving the payload structure through the WithPayload generic type.
How are concurrent dApp requests sequenced to avoid race conditions?
The AsyncRequestQueue class in asyncRequestQueue.ts implements a first-in-first-out queue that processes only one UI-side request at a time. When multiple dApps or rapid successive calls request wallet operations, the background script enqueues each as an AsyncRequest and awaits explicit resolution. This guarantees that user approval flows—such as transaction signing—complete sequentially rather than overlapping, preventing state corruption and user interface conflicts.
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 →