# How Nautilus Wallet Implements HD Wallet Derivation (BIP-32/39)

> Discover how Nautilus Wallet uses the Fleet SDK for BIP-32/39 HD wallet derivation. Learn about Ergo address encoding, neutering, and Yoroi compatibility.

- Repository: [Nautilus Team/nautilus-wallet](https://github.com/nautls/nautilus-wallet)
- Tags: deep-dive
- Published: 2026-03-07

---

**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`](https://github.com/nautls/nautilus-wallet/blob/main/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`](https://github.com/nautls/nautilus-wallet/blob/main/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:

```typescript
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`](https://github.com/nautls/nautilus-wallet/blob/main/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`](https://github.com/nautls/nautilus-wallet/blob/main/src/chains/ergo/hdKey.ts):

```typescript
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`](https://github.com/nautls/nautilus-wallet/blob/main/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-sdk` for low-level BIP-32/39 cryptography, avoiding custom cryptographic implementations.
- **Wrapper Location**: All HD operations are encapsulated in [`src/chains/ergo/hdKey.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/chains/ergo/hdKey.ts), exposing an `HdKey` class that wraps `ErgoHDKey`.
- **Security Features**: The `neutered()` method and `fromPublicKey` constructor 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`](https://github.com/nautls/nautilus-wallet/blob/main/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.