How Nautilus Wallet Implements Address Discovery and Scanning: HD Wallet Architecture Explained
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. 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:
// 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. 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:
// 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 manages the efficient retrieval and on-demand derivation of address batches:
// 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. This method implements a while-loop that continues scanning until the gap limit is reached or the wallet ID changes:
// 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. 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 respects user preferences for address reuse:
// 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:
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:
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:
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
HdKeyclass insrc/chains/ergo/hdKey.tsto deterministically derive addresses from extended public keys, withderiveAddresses()enabling batch generation. -
Chunked Scanning: The wallet implements a gap limit of 20 addresses (
CHUNK_DERIVE_LENGTHinsrc/constants/ergo.ts), scanning addresses in chunks until it encounters a complete batch of unused addresses. -
Efficient Derivation: The
getOrDerivehelper insrc/stores/walletStore.tsminimizes computation by reusing cached addresses and only deriving new ones when necessary. -
GraphQL Integration: The
sync()loop insrc/stores/walletStore.tsqueriesgraphQLService.getAddressesInfoto determine address usage, persisting results to IndexedDB. -
Change Address Logic: The wallet supports both privacy-focused address rotation (
avoidAddressReuse) and fixed change addresses via thechangeAddresscomputed 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. 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. 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. 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) 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, 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.
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 →