How Nautilus Wallet Handles Wallet Synchronization with the Ergo Blockchain
Nautilus Wallet synchronizes with the Ergo blockchain by running a periodic sync routine that derives addresses, queries Ergo-GraphQL for UTXO and asset data, detects changes, and persists deltas to IndexedDB, triggered automatically on new block detection or manually via the wallet store.
The nautls/nautilus-wallet repository implements a robust, reactive pipeline to keep local wallet state consistent with the live Ergo network. Understanding this wallet synchronization mechanism is essential for developers building on Ergo or contributing to the Nautilus codebase.
The Sync Architecture Overview
At the heart of the synchronization logic lies src/stores/walletStore.ts, a Pinia store that orchestrates a four-stage pipeline. The process ensures that address derivation, blockchain queries, change detection, and persistence happen atomically and efficiently.
The sync routine respects a MIN_SYNC_INTERVAL constant (defined in src/constants/intervals.ts, default approximately 30 seconds) to prevent excessive network calls:
if (Date.now() - privateState.lastSynced < MIN_SYNC_INTERVAL) return setSyncing(false);
Stage 1: Address Derivation
Before querying the blockchain, the wallet must ensure it has derived enough addresses to detect all UTXOs. The sync() method calls getOrDerive() to obtain a batch of addresses using the HD key pool.
- Source:
src/stores/walletStore.ts(viagetOrDerive) - HD Logic:
src/chains/ergo/hdKey.ts
The derivation process uses CHUNK_DERIVE_LENGTH to determine how many addresses to generate at once. If the local cache lacks sufficient addresses, HdKey.deriveAddresses() creates the missing ones deterministically from the master key stored in the per-wallet hdKeyPool.
Stage 2: Blockchain Data Retrieval via Ergo-GraphQL
With addresses prepared, the wallet queries the Ergo network through GraphQL. The graphQLService.getAddressesInfo method (located in src/chains/ergo/services/graphQlService.ts) executes the ADDRESS_INFO_QUERY against a public Ergo-GraphQL endpoint (e.g., explore.sigmaspace.io).
The GraphQL response provides for each address:
- A
usedflag indicating if the address has transaction history - A complete list of assets (including native ERG and tokens)
This approach minimizes data transfer by batching address queries and leveraging GraphQL’s selective field retrieval.
Stage 3: Change Detection and Persistence
Raw blockchain data alone does not update the UI. The getChanges() function in walletStore.ts performs a diff between the newly fetched address/asset rows and the existing privateState.addresses and privateState.assets.
The change detection identifies:
- Changed addresses: Updated balance or usage status
- Changed assets: Modified token quantities
- Disappeared assets: Tokens spent or moved
Persistence happens via IndexedDB bulk operations:
addressesDbService.bulkPut(src/database/addressesDbService.ts)assetsDbService.bulkPutandassetsDbService.bulkDelete(src/database/assetsDbService.ts)
After persistence, assetsStore.loadMetadata() fetches EIP-4 metadata (names, decimals, artwork) for any newly discovered token IDs. Finally, the Pinia store patches its reactive arrays using patchAddresses and patchAssets, updates lastSynced, and clears the hasOldUtxos flag.
Stage 4: Health Monitoring and UTXO Age Checks
The synchronization includes a health check mechanism. The checkOldUtxos() function uses the same GraphQL service (checkBoxesOlderThan) to query for UTXOs older than HEALTHY_BLOCKS_AGE.
This check exposes a health.hasOldUtxos flag, warning users if their wallet contains spent or stale boxes that might indicate synchronization issues or the need for consolidation.
Trigger Mechanisms: Automatic and Manual Sync
The wallet supports both reactive and imperative synchronization triggers.
Automatic Trigger:
A watch on chain.height in src/stores/chainStore.ts invokes sync() whenever a new block is detected:
watch(() => chain.height, () => {
if (appStore.loading || privateState.loading || !appStore.settings.lastOpenedWalletId) return;
sync();
});
Manual Trigger:
The public load(walletId, { syncInBackground }) action allows components to force synchronization:
if (opt.syncInBackground) {
sync(); // fire-and-forget
} else {
await sync(); // wait for completion
}
Code Examples for Developers
Force a Sync from a Component
import { useWalletStore } from '@/stores/walletStore';
const wallet = useWalletStore();
// Force immediate resynchronisation
await wallet.load(wallet.id, { syncInBackground: false });
Observe the Syncing Flag for UI Spinners
import { useWalletStore } from '@/stores/walletStore';
import { watch } from 'vue';
const wallet = useWalletStore();
watch(() => wallet.syncing, (now) => {
console.log(`Sync is ${now ? 'running' : 'idle'}`);
});
Read the Latest Block Height
import { useChainStore } from '@/stores/chainStore';
const chain = useChainStore();
console.log('Current Ergo block height:', chain.height);
Summary
- Periodic Sync Routine: The
sync()function insrc/stores/walletStore.tsorchestrates wallet synchronization with the Ergo blockchain, respecting aMIN_SYNC_INTERVALto prevent excessive calls. - Four-Stage Pipeline: The process derives HD addresses in chunks, queries Ergo-GraphQL for UTXO and asset data, detects changes against local IndexedDB state, and persists deltas while updating the UI reactively.
- Automatic & Manual Triggers: Synchronization fires automatically when
chain.heightchanges (new block detection) or manually viaload()with configurable background/foreground execution. - Health Monitoring: The
checkOldUtxos()function monitors for stale UTXOs usingHEALTHY_BLOCKS_AGEto flag potential synchronization issues.
Frequently Asked Questions
How does Nautilus Wallet prevent excessive synchronization calls to the Ergo blockchain?
Nautilus implements a rate-limiting mechanism using the MIN_SYNC_INTERVAL constant defined in src/constants/intervals.ts. The sync() function checks if the time elapsed since privateState.lastSynced is less than this interval (approximately 30 seconds) and exits early if the threshold hasn't been met, preventing network spam and reducing load on public GraphQL endpoints.
What happens if the GraphQL endpoint returns new token IDs that the wallet hasn't seen before?
When the sync process detects new token IDs during the change detection phase, it triggers assetsStore.loadMetadata() to fetch EIP-4 compliant metadata for those tokens. This metadata includes token names, decimal places, and artwork URLs. The metadata is then cached and displayed in the UI, ensuring users see human-readable token information rather than raw token IDs.
Can developers force a wallet synchronization to complete before executing a transaction?
Yes, developers can force synchronous wallet loading by calling wallet.load(walletId, { syncInBackground: false }). When syncInBackground is set to false, the method awaits the completion of the sync() function before resolving, ensuring that the wallet state is fully up-to-date with the Ergo blockchain before proceeding with transaction construction or balance checks.
How does the wallet detect if it contains old or potentially spent UTXOs?
The wallet runs a health check via checkOldUtxos() in src/stores/walletStore.ts, which queries the GraphQL service using checkBoxesOlderThan to find UTXOs exceeding HEALTHY_BLOCKS_AGE. If old boxes are found, the health.hasOldUtxos flag is set to true, alerting users to potential synchronization issues or the need to consolidate UTXOs to maintain wallet health.
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 →