# How Nautilus Wallet Implements Address Discovery and Scanning: HD Wallet Architecture Explained

> Learn how Nautilus Wallet implements address discovery and scanning with its HD wallet architecture. Discover addresses in batches and efficiently scan the blockchain.

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

---

**Nautilus Wallet discovers and scans blockchain addresses using a chunked HD derivation algorithm that derives addresses in batches of 20 and queries a GraphQL endpoint to detect usage, stopping when it encounters a gap limit of unused addresses.**

Nautilus Wallet is an open-source cryptocurrency wallet for the Ergo blockchain that implements hierarchical deterministic (HD) address generation with sophisticated scanning capabilities. Understanding how address discovery and scanning works in this codebase reveals a production-ready implementation of BIP-32/44 standards optimized for the UTXO model.

## HD Key Derivation and Address Generation

### The HdKey Class and Deterministic Derivation

At the core of address discovery lies the `HdKey` class in [`src/chains/ergo/hdKey.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/chains/ergo/hdKey.ts). This class wraps an extended public key (xpub) and provides deterministic derivation methods that generate addresses without exposing private keys.

The class exposes two primary derivation methods:

```typescript
// src/chains/ergo/hdKey.ts
// Derive a single address at specific index
public deriveAddress(index: number): IndexedAddress { … }

// Derive a batch of addresses for efficient scanning
public deriveAddresses(count: number, offset = 0): IndexedAddress[] { … }

```

The `deriveAddresses` method is particularly critical for scanning performance, as it allows the wallet to generate multiple addresses in a single operation rather than iterating individual derivations.

### Object Pooling for Performance

To prevent redundant instantiation of `HdKey` objects, Nautilus implements an object pool pattern in [`src/common/objectPool.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/common/objectPool.ts). The wallet maintains a global `hdKeyPool` that caches `HdKey` instances keyed by the wallet's public key.

When the wallet store needs to derive addresses, it retrieves the deriver via `hdKeyPool.get(publicKey)`, ensuring that the computationally expensive HD key initialization happens only once per wallet session.

## Chunked Address Scanning Algorithm

### The Gap Limit and Chunk Configuration

Nautilus Wallet implements a gap limit mechanism to determine when to stop scanning for new addresses. The gap limit—the number of consecutive unused addresses that triggers scan termination—is defined in [`src/constants/ergo.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/constants/ergo.ts):

```typescript
// src/constants/ergo.ts
export const CHUNK_DERIVE_LENGTH = 20;

```

This constant determines both the derivation batch size and the gap limit. The wallet derives and scans addresses in chunks of 20, continuing until it encounters a complete chunk where no addresses are marked as used.

### The getOrDerive Helper Function

The `getOrDerive` function in [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts) manages the efficient retrieval and on-demand derivation of address batches:

```typescript
// src/stores/walletStore.ts
function getOrDerive(derived, deriver, count, offset) {
  const chunk = derived.slice(offset, offset + count);
  if (chunk.length < count) {
    const remaining = count - chunk.length;
    chunk.push(...deriver.deriveAddresses(remaining, offset + chunk.length));
  }
  return chunk;
}

```

This helper ensures that the wallet only derives new addresses when the cached `derived` array doesn't contain enough entries to satisfy the requested chunk size, optimizing memory and computation.

### The Main Sync Loop

The core address discovery logic resides in the `sync()` method within [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts). This method implements a while-loop that continues scanning until the gap limit is reached or the wallet ID changes:

```typescript
// src/stores/walletStore.ts – sync()
while (keepChecking && walletId === privateState.id) {
  const derived = getOrDerive(privateState.addresses, deriver, CHUNK_DERIVE_LENGTH, offset);
  const info = await graphQLService.getAddressesInfo(derived.map(x => x.script));

  // store addresses + assets …
  offset += derived.length;
  keepChecking = info.some(x => x.used);   // stop when no address in the chunk is used
}

```

The loop derives a chunk of addresses, queries the GraphQL service for usage information, persists the results to IndexedDB, and checks if any address in the chunk is used. If no addresses are used, `keepChecking` becomes false and the scan terminates.

## GraphQL Integration and Address State Tracking

The actual blockchain scanning is delegated to `graphQLService` in [`src/chains/ergo/services/graphQlService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/chains/ergo/services/graphQlService.ts). This service provides the `getAddressesInfo` method that accepts an array of address scripts (Ergo addresses) and returns metadata including the `used` boolean flag.

When the sync loop receives the response, it updates the local state and persists address information—including the used/unused state and associated assets—to IndexedDB. This allows the wallet to maintain a complete history of discovered addresses without rescanning the entire blockchain on every startup.

## Change Address Selection Logic

Beyond discovery, Nautilus implements sophisticated logic for selecting **change addresses** (addresses that receive leftover funds from transactions). The `changeAddress` computed property in [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts) respects user preferences for address reuse:

```typescript
// src/stores/walletStore.ts – changeAddress computed
const changeAddress = computed(() => {
  const address = settings.value.avoidAddressReuse
    ? addresses.value.find(a => a.state === AddressState.Unused)
    : addresses.value.find(a => a.index === settings.value.defaultChangeIndex);
  if (!address) throw new Error(`Change address not found`);
  return address;
});

```

When `avoidAddressReuse` is enabled, the wallet selects the first unused address from the discovered set. Otherwise, it uses the address at the configured `defaultChangeIndex`, providing flexibility for privacy-conscious users versus those prioritizing address consistency.

## Code Examples

### Deriving a New Address

To manually trigger address derivation from a Vue component:

```typescript
import { useWalletStore } from '@/stores/walletStore';

async function createNewAddress() {
  const wallet = useWalletStore();
  await wallet.deriveNewAddress();   // triggers derivation + DB persistence
  console.log('New address added to DB');
}

```

### Triggering a Wallet Scan

To force a complete rescan of the wallet addresses:

```typescript
import { useWalletStore } from '@/stores/walletStore';

async function refreshAll() {
  const wallet = useWalletStore();
  // `load` internally calls `sync` which performs the chunked scan
  await wallet.load(wallet.id);
}

```

### Retrieving the Change Address

To access the currently selected change address for transaction construction:

```typescript
import { useWalletStore } from '@/stores/walletStore';

function getChange() {
  const wallet = useWalletStore();
  const { script, index } = wallet.changeAddress;
  console.log(`Change address #${index}: ${script}`);
}

```

## Summary

- **HD Key Management**: Nautilus uses the `HdKey` class in [`src/chains/ergo/hdKey.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/chains/ergo/hdKey.ts) to deterministically derive addresses from extended public keys, with `deriveAddresses()` enabling batch generation.

- **Chunked Scanning**: The wallet implements a gap limit of 20 addresses (`CHUNK_DERIVE_LENGTH` in [`src/constants/ergo.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/constants/ergo.ts)), scanning addresses in chunks until it encounters a complete batch of unused addresses.

- **Efficient Derivation**: The `getOrDerive` helper in [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts) minimizes computation by reusing cached addresses and only deriving new ones when necessary.

- **GraphQL Integration**: The `sync()` loop in [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts) queries `graphQLService.getAddressesInfo` to determine address usage, persisting results to IndexedDB.

- **Change Address Logic**: The wallet supports both privacy-focused address rotation (`avoidAddressReuse`) and fixed change addresses via the `changeAddress` computed property.

## Frequently Asked Questions

### What is the gap limit in Nautilus Wallet?

The gap limit in Nautilus Wallet is set to **20 addresses**, defined by the `CHUNK_DERIVE_LENGTH` constant in [`src/constants/ergo.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/constants/ergo.ts). This means the wallet will continue scanning as long as it finds used addresses, and stops only when it encounters a complete chunk of 20 consecutive unused addresses. This approach balances comprehensive discovery with network efficiency.

### How does Nautilus Wallet know when to stop scanning?

The wallet uses a boolean flag called `keepChecking` in the `sync()` method of [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts). After scanning each chunk of addresses via `graphQLService.getAddressesInfo`, the wallet checks if any address in the chunk has `used === true`. If at least one address is used, `keepChecking` remains true and the scan continues to the next chunk. When a complete chunk contains no used addresses, `keepChecking` becomes false and the scan terminates.

### What is the difference between address derivation and address scanning?

**Address derivation** is the local, cryptographic process of generating new addresses from the wallet's extended public key using the `HdKey` class in [`src/chains/ergo/hdKey.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/chains/ergo/hdKey.ts). This happens entirely offline and creates the actual Ergo address strings. **Address scanning** is the online process of querying the blockchain (via GraphQL in [`src/chains/ergo/services/graphQlService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/chains/ergo/services/graphQlService.ts)) to determine which derived addresses have actually been used to receive funds. Derivation creates the addresses; scanning discovers which ones contain assets.

### How does the wallet handle change addresses?

Nautilus Wallet handles change addresses through the `changeAddress` computed property in [`src/stores/walletStore.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/stores/walletStore.ts), which respects the `avoidAddressReuse` setting. When `avoidAddressReuse` is enabled, the wallet selects the first unused address (where `state === AddressState.Unused`) from the discovered set, ensuring each transaction returns change to a fresh address for privacy. When disabled, it consistently uses the address at `settings.defaultChangeIndex`, providing a stable change address for users who prefer address reuse.