nautechsystems/nautilus_trader · error · anyhow::Error

Wallet balance snapshot contains duplicate token addresses:

Error message

Wallet balance snapshot contains duplicate token addresses: {}

What it means

WalletBalanceSnapshot::validate_token_addresses rejects a snapshot whose token holdings contain the same token address more than once. Duplicate addresses make balance attribution ambiguous, so the snapshot is considered corrupt and an anyhow error is raised instead of returning balances.

Source

Thrown at crates/model/src/defi/wallet.rs:226

        }

        Ok(balances)
    }

    fn validate_token_addresses(&self) -> anyhow::Result<()> {
        let mut token_addresses = HashSet::with_capacity(self.token_balances.len());
        let mut duplicates = Vec::new();

        for balance in &self.token_balances {
            if !token_addresses.insert(balance.token.address) {
                duplicates.push(balance.token.address);
            }
        }

        if !duplicates.is_empty() {
            duplicates.sort_unstable();
            duplicates.dedup();
            anyhow::bail!(
                "Wallet balance snapshot contains duplicate token addresses: {}",
                format_addresses(&duplicates)
            );
        }

        let mut missing = self
            .token_universe
            .difference(&token_addresses)
            .copied()
            .collect::<Vec<_>>();
        let mut unexpected = token_addresses
            .difference(&self.token_universe)
            .copied()
            .collect::<Vec<_>>();
        missing.sort_unstable();
        unexpected.sort_unstable();

        match (missing.is_empty(), unexpected.is_empty()) {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Normalize all token addresses (same casing/checksum) and deduplicate before constructing the snapshot.
  2. When merging balance sources, fold duplicate addresses into one entry by summing or replacing balances instead of appending.
  3. Log and inspect the deduplicated address list from the error message to find which source introduced the duplicate.

Example fix

// before
let mut balances = Vec::new();
for feed in feeds { balances.extend(feed.balances()); }
// after
use std::collections::HashMap;
let mut balances: HashMap<Address, _> = HashMap::new();
for feed in feeds {
    for (addr, bal) in feed.balances() {
        *balances.entry(addr).or_default() += bal;
    }
}
Defensive patterns

Strategy: validation

Validate before calling

fn has_no_duplicates(addrs: &[Address]) -> bool {
    let unique: std::collections::HashSet<_> = addrs.iter().collect();
    unique.len() == addrs.len()
}

Prevention

When it happens

Trigger: Calling WalletBalanceSnapshot::account_balances (which invokes validate_token_addresses) when the snapshot's balances map was built with the same token address inserted twice — e.g. aggregating balances from two sources that both include the same ERC-20 address.

Common situations: Merging balance feeds from multiple RPC providers or chains, manually constructing a snapshot from JSON where one token appears under duplicate keys that were normalized to the same address, or case-insensitive hex addresses not being checksum-normalized before deduplication.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/a51dcb733dcafbd3. Report an issue: GitHub.