Transaction Signing and Submission in Nautilus Wallet: A Complete Technical Guide
Nautilus Wallet orchestrates transaction signing and submission through a multi-layered architecture where dApps invoke window.ergoConnector.nautilus methods that flow through injected scripts, background processes, and cryptographic provers before broadcasting via GraphQL to the Ergo blockchain.
Nautilus Wallet is an open-source browser extension for the Ergo blockchain that provides secure transaction signing and submission capabilities for decentralized applications. According to the nautls/nautilus-wallet source code, the wallet implements a sophisticated message-passing architecture that isolates private keys within the extension background while enabling seamless dApp integration. This technical guide examines the complete pipeline from API invocation to blockchain confirmation, referencing specific implementation files and method signatures.
The Injected API: dApp Entry Points
Decentralized applications interact with Nautilus Wallet through the injected API exposed in src/extension/content-scripts/injected.ts. The NautilusErgoApi class provides two primary methods for transaction handling.
sign_tx(transaction) accepts an EIP-12 unsigned transaction and forwards it to the extension background via ExternalRequest.SignTx. This method triggers the cryptographic signing flow where user confirmation is required.
submit_tx(transaction) accepts a signed transaction and forwards it via ExternalRequest.SubmitTransaction for network broadcasting. This separation allows dApps to either submit immediately after signing or handle the signed transaction separately.
// src/extension/content-scripts/injected.ts
async sign_tx(transaction: EIP12UnsignedTransaction) {
return handle(await sendMessage(ExternalRequest.SignTx, { transaction }, CONTENT_SCRIPT));
}
async submit_tx(transaction: SignedTransaction) {
return handle(
await sendMessage(ExternalRequest.SubmitTransaction, { transaction }, CONTENT_SCRIPT)
);
}
Message Routing: Bridging Content Scripts and Background
The content script in src/extension/content-scripts/contentScript.ts acts as a secure bridge between the dApp's execution context and the privileged extension background. It receives external requests from the injected API and repackages them as internal requests using the InternalRequest enum.
This architectural pattern ensures that malicious web pages cannot directly access wallet internals. The content script validates message origins before forwarding:
// src/extension/content-scripts/contentScript.ts
onMessage(ExternalRequest.SignTx, async ({ data }) => {
return await sendMessage(InternalRequest.SignTx, { payload, ...data }, BACKGROUND);
});
onMessage(ExternalRequest.SubmitTransaction, async ({ data }) => {
return await sendMessage(InternalRequest.SubmitTransaction, { payload, ...data }, BACKGROUND);
});
Background Processing: Authorization and UI Coordination
The background script at src/extension/background/background.ts handles privileged operations using onMessageAuth wrappers that verify authentication state before processing.
Transaction Signing Flow
When InternalRequest.SignTx arrives, the background opens a secure UI window via openWindow() where users review transaction details. The request enters the AsyncRequestQueue system, creating a promise that resolves only after user confirmation or rejection:
// src/extension/background/background.ts
onMessageAuth(InternalRequest.SignTx, async (msg) => {
if (!msg.data.transaction) return invalidRequest("Invalid params.");
return await openWindow(InternalRequest.SignTx, msg.data, msg.sender.tabId);
});
Transaction Submission Flow
For InternalRequest.SubmitTransaction, the background immediately invokes the GraphQL service without UI interaction, assuming the transaction is already signed. The handler includes error mapping to standardized TxSendErrorCode values:
// src/extension/background/background.ts
onMessageAuth(InternalRequest.SubmitTransaction, async (msg, walletId) => {
if (!msg.data.transaction) return invalidRequest("Invalid params.");
try {
const response = await graphQLService.submitTransaction(msg.data.transaction, walletId);
return success(response.transactionId);
} catch (e) {
return error(TxSendErrorCode.Refused, (e as Error).message);
}
});
Cryptographic Signing: The Prover Architecture
The actual cryptographic operations occur in src/chains/ergo/transaction/prover.ts. The Prover class abstracts both software-based signing and hardware wallet (Ledger) interactions.
Software Wallet Signing
For software wallets, the Prover derives secret keys from the wallet's seed and signs transactions in-memory using the wallet.sign_transaction method from the Ergo WASM bindings.
Hardware Wallet Signing
For Ledger devices, the Prover instantiates an ErgoLedgerApp, gathers input boxes and witnesses, and requests cryptographic proofs from the connected hardware device without exposing private keys to the browser context.
The unified interface exposes signTransaction() which returns a EIP-12 compatible signed transaction object:
// src/chains/ergo/transaction/prover.ts
async signTransaction(unsignedTx: EIP12UnsignedTransaction): Promise<SignedTransaction> {
const { tx, inputs, dataInputs } = this.#parseUnsignedTx(unsignedTx);
const signed = await this.#signTx(tx, inputs, dataInputs);
return signed.to_js_eip12();
}
Network Broadcasting: GraphQL Service and Retry Logic
The graphQLService in src/chains/ergo/services/graphQlService.ts handles communication with Ergo nodes via GraphQL endpoints. The submitTransaction method implements resilience patterns for blockchain interaction.
Exponential Backoff: If submission fails because inputs remain in the local UTXO set (indicating a race condition or unconfirmed parent transaction), the service automatically retries with exponential backoff.
UTXO Synchronization: Upon successful broadcast, the service updates the local UTXO database via utxosDbService.addFromTx() to reflect spent and created boxes:
// src/chains/ergo/services/graphQlService.ts
override async submitTransaction(
signedTransaction: SignedTransaction,
walletId?: number
): Promise<TransactionEvaluationSuccess> {
const result = await super.submitTransaction(signedTransaction);
if (!result.success) throw new Error(result.message);
else if (walletId) utxosDbService.addFromTx(signedTransaction, walletId);
return result;
}
Practical Implementation: Complete dApp Integration
Below is a production-ready example demonstrating the full signing and submission flow using the Fleet SDK and Nautilus Wallet:
import { TransactionBuilder } from '@fleet-sdk/common';
// 1. Build an unsigned EIP-12 transaction
const unsigned = new TransactionBuilder(currentHeight)
.addOutput({ address: receiver, value: 1_000_000n })
.build()
.toEIP12Object();
// 2. Request cryptographic signing (opens Nautilus confirmation window)
const signed = await window.ergoConnector.nautilus.sign_tx(unsigned);
// 3. Broadcast to the Ergo network
const txId = await window.ergoConnector.nautilus.submit_tx(signed);
console.log('Transaction submitted, id:', txId);
This implementation automatically routes through src/extension/content-scripts/injected.ts → contentScript.ts → background.ts → prover.ts → graphQlService.ts as detailed above.
Summary
- Nautilus Wallet isolates cryptographic operations from dApps through a three-layer message passing architecture: injected API, content script bridge, and privileged background context.
- Signing requests (
sign_tx) trigger user confirmation windows managed byAsyncRequestQueuein the background script before the Prover class generates cryptographic proofs. - Submission requests (
submit_tx) bypass UI confirmation and route directly tographQLService.submitTransaction, which implements exponential backoff retry logic for UTXO conflicts. - Hardware wallets are supported through the
ErgoLedgerAppabstraction within the Prover, ensuring private keys never enter the browser context. - Source files
src/extension/content-scripts/injected.ts,src/extension/background/background.ts, andsrc/chains/ergo/transaction/prover.tsdefine the critical integration points for developers building Ergo dApps.
Frequently Asked Questions
How does Nautilus Wallet protect private keys during transaction signing?
Private keys remain confined to the extension background context or Ledger hardware device. The Prover class in src/chains/ergo/transaction/prover.ts either uses WASM-bound wallet objects for software signing or communicates with Ledger via the ErgoLedgerApp interface. dApps receive only the final signed transaction object, never accessing key material.
What happens if transaction submission fails due to network issues?
The graphQLService.submitTransaction method in src/chains/ergo/services/graphQlService.ts implements retry logic with exponential backoff when inputs remain in the local UTXO set. If the Ergo node rejects
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 →