How Nautilus Wallet Implements HD Wallet Derivation (BIP-32/39)
Nautilus Wallet delegates BIP-32/39 cryptographic operations to the @fleet-sdk library while wrapping them in a thin HdKey utility that adds Ergo-specific address encoding, read-only neutering, and Yoroi compatibility layers.
The nautls/nautilus-wallet repository implements hierarchical deterministic (HD) wallet functionality by building atop proven cryptographic primitives rather than reinventing them. This article examines the implementation strategy for HD wallet derivation (BIP-32/39) in Nautilus Wallet, focusing on how the codebase wraps low-level SDK operations to provide a secure, wallet-specific API.
Architecture: Delegation to @fleet-sdk
Rather than implementing BIP-32 tree-derived key structures and BIP-39 mnemonic handling from scratch, Nautilus Wallet imports these capabilities from @fleet-sdk, a JavaScript library that provides Ergo blockchain primitives. The wallet encapsulates the low-level ErgoHDKey class within a higher-level HdKey utility located in src/chains/ergo/hdKey.ts.
This wrapper pattern allows the application to:
- Maintain agnostic consumption of HD operations throughout UI components and dApp connectors
- Inject Ergo-specific network constants (
MAINNET/TESTNET) during address encoding - Provide convenience methods for batch operations and compatibility normalization
Mnemonic and Master Key Generation
The entry point for standard wallet creation begins with BIP-39 mnemonic processing. In src/chains/ergo/hdKey.ts, the static method fromMnemonic forwards the mnemonic phrase to ErgoHDKey.fromMnemonic, which internally executes BIP-39 seed generation and BIP-32 master key derivation.
According to lines 27-29 of the source file, the implementation is a thin pass-through that initializes the underlying SDK class:
const hd = await HdKey.fromMnemonic(mnemonic);
This creates a fully capable HD wallet instance containing both private and public key material, enabling subsequent child derivation operations.
Public-Key Only and Neutered Modes
For read-only scenarios and dApp connections that require only public information, the wallet implements two distinct import strategies.
Importing Extended Public Keys
The fromPublicKey static method (lines 31-38) accepts either an extended public key string or an object containing raw publicKey and chainCode bytes. It instantiates an ErgoHDKey that contains no private data, creating a neutered HD key suitable for address derivation without signing capabilities.
Runtime Neutering
The neutered() method (lines 20-25) provides runtime security by wiping private keys from an existing instance. It calls wipePrivateData() on the underlying ErgoHDKey, returning a new HdKey instance that preserves the public derivation structure while eliminating all private key material. This operation is essential when exposing wallet data to external connectors or read-only contexts.
Child Key and Address Derivation
The HdKey class exposes several methods for deriving hierarchical keys and converting them to Ergo addresses.
Private Key Derivation
The derivePrivateKey(index) method (lines 70-75) retrieves child keys via this.#change.deriveChild(index).privateKey. If the derived key lacks a private component—which occurs when attempting hardened derivation from a public-only parent—the method throws an error. This behavior preserves the BIP-32 contract that hardened paths require access to the parent private key.
Address Generation
For standard receiving addresses, deriveAddress(index) (lines 77-80) obtains a child HD key and calls .address.encode(NETWORK), leveraging the Ergo address encoding defined in the SDK combined with network constants imported from src/constants/ergo.ts.
Batch Address Generation
To support BIP-44 gap-limit scanning, the deriveAddresses(count, offset) method (lines 82-89) iterates over a specified range, reusing deriveAddress for each index. This generates arrays of address objects containing both the derivation index and the encoded script address, optimized for wallet synchronization workflows.
Extended Public Key Normalization
Nautilus Wallet includes compatibility adjustments for other Ergo clients, specifically Yoroi. The raw extended public key from ErgoHDKey contains fingerprint and child number metadata in bytes 4-11 that Yoroi does not expect.
The normalizeExtendedKey method (lines 66-68) zeros these bytes to strip the extraneous fields, ensuring cross-wallet compatibility. Additionally, the extendedPublicKey getter (lines 52-59) implements lazy decoding—it decodes the base58check string only once and caches the normalized Uint8Array for subsequent access, optimizing performance during repeated dApp interactions.
Practical Usage Examples
The following TypeScript examples demonstrate typical HdKey operations as implemented in src/chains/ergo/hdKey.ts:
import HdKey from '@/chains/ergo/hdKey';
// 1️⃣ Create an HD wallet from a BIP‑39 mnemonic
const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const hd = await HdKey.fromMnemonic(mnemonic);
// 2️⃣ Derive the first receiving address (index 0)
const { index, script } = hd.deriveAddress(0);
console.log(`Address #${index}: ${script}`);
// 3️⃣ Generate a batch of 5 addresses (gap‑limit style)
const batch = hd.deriveAddresses(5);
batch.forEach(({ index, script }) => console.log(`#${index} → ${script}`));
// 4️⃣ Export a neutered (public‑only) instance for a dApp connection
const publicOnly = hd.neutered();
console.log('Extended public key (normalized):', publicOnly.extendedPublicKey);
Unit tests in tests/unit/hdKey.spec.ts verify these derivation paths, ensuring the wrapper correctly interfaces with @fleet-sdk while maintaining the expected BIP-32/39 semantics.
Summary
- Delegation Strategy: Nautilus Wallet relies on
@fleet-sdkfor low-level BIP-32/39 cryptography, avoiding custom cryptographic implementations. - Wrapper Location: All HD operations are encapsulated in
src/chains/ergo/hdKey.ts, exposing anHdKeyclass that wrapsErgoHDKey. - Security Features: The
neutered()method andfromPublicKeyconstructor enable read-only wallet modes by eliminating private key access. - Address Derivation: Child keys convert to Ergo addresses using SDK encoding with network-specific constants from
src/constants/ergo.ts. - Compatibility Layer: Extended public keys undergo normalization (bytes 4-11 zeroed) to ensure compatibility with Yoroi and other Ergo wallets.
Frequently Asked Questions
What library handles the BIP-32/39 cryptography in Nautilus Wallet?
The cryptographic heavy lifting is performed by @fleet-sdk (specifically @fleet-sdk/crypto and @fleet-sdk/wallet packages), which provide the ErgoHDKey class implementing BIP-32 tree derivation and BIP-39 mnemonic handling. Nautilus Wallet wraps these primitives in the HdKey utility to add wallet-specific functionality.
How does Nautilus Wallet support read-only wallet imports?
The wallet supports read-only mode through two mechanisms: the fromPublicKey static method creates an HD instance from an extended public key or raw public key components, and the neutered() instance method removes private data from an existing wallet via wipePrivateData(). Both approaches yield an HdKey instance capable of address derivation but incapable of signing transactions.
Why does Nautilus Wallet normalize extended public keys?
Normalization ensures compatibility with Yoroi and other Ergo ecosystem wallets. The normalizeExtendedKey method zeros bytes 4-11 of the serialized extended public key to remove the fingerprint and child number fields that @fleet-sdk includes but Yoroi expects to be empty. This allows seamless public key sharing between different Ergo wallet implementations.
Can Nautilus Wallet derive hardened child keys?
Yes, but only from instances containing private key material. The derivePrivateKey method enforces the BIP-32 specification by throwing an error if the derived child key lacks a private component, which occurs when attempting hardened derivation from a neutered or public-only parent. This preserves the cryptographic requirement that hardened paths require the parent private key.
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 →