unionlabs/union · error · anyhow::Error

no balance for denom {}

Error message

no balance for denom {}

What it means

`CosmosClient::get_balance` issues a `/cosmos.bank.v1beta1.Query/Balance` grpc_abci_query and unwraps `resp.balance`. Cosmos SDK returns an empty balance when the account holds no Coin for that denom, and this code converts that None into an error rather than zero.

Source

Thrown at tools/union-test/src/cosmos.rs:625

    pub async fn get_balance(
        &self,
        address: impl Into<String>,
        denom: &str,
    ) -> anyhow::Result<protos::cosmos::base::v1beta1::Coin> {
        let req = QueryBalanceRequest {
            address: address.into(),
            denom: denom.to_string(),
        };
        let resp: QueryBalanceResponse = self
            .rpc
            .client()
            .grpc_abci_query("/cosmos.bank.v1beta1.Query/Balance", &req, None, false)
            .await?
            .into_result()?
            .unwrap();
        resp.balance
            .ok_or_else(|| anyhow::anyhow!("no balance for denom {}", denom))
    }

    pub async fn send_cosmwasm_transaction_with_retry(
        &self,
        contract: Addr,
        msg: (Vec<u8>, Vec<Coin>),
        signer: &LocalSigner,
    ) -> Option<Result<TxResponse, BroadcastTxCommitError>> {
        let max_retries = 5;
        for attempt in 1..=max_retries {
            let outcome = self
                .send_cosmwasm_transaction(contract.clone(), msg.clone(), signer)
                .await;

            if let Some(Ok(_)) = &outcome {
                return outcome;
            }

View on GitHub (pinned to 031785bb6d)

Solutions

  1. List the account's actual balances (`/cosmos.bank.v1beta1.Query/AllBalances`) and use the exact denom string returned
  2. Fund the account (faucet or bank send) before the step that requires the balance
  3. If zero is a legitimate state in your flow, branch on the Option instead of calling this helper

Example fix

// before
let coin = client.get_balance(&addr, "uatom").await?; 
// after
let amount = client
    .get_balance(&addr, denom)
    .await
    .ok()
    .flatten_amount_if_any() // Option<Coin>
    .unwrap_or_else(|| Coin::default_for(denom));
Defensive patterns

Strategy: validation

Validate before calling

// list real denoms before querying a specific one
let balances: QueryAllBalancesResponse = client
    .rpc.client()
    .grpc_abci_query("/cosmos.bank.v1beta1.Query/AllBalances", &QueryAllBalancesRequest { address: addr.clone().into(), ..Default::default() }, None, false)
    .await?.into_result()?.unwrap();
anyhow::ensure!(balances.balances.iter().any(|c| c.denom == denom), "denom {denom} not held by {addr}");

Try / catch

match client.get_balance(&addr, denom).await {
    Ok(coin) => Ok(coin),
    Err(e) if e.to_string().starts_with("no balance for denom") => {
        // decide explicitly: is zero acceptable, or should the account be funded first?
        fund_if_needed(&addr, denom).await?;
        client.get_balance(&addr, denom).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Querying a denom the account has never held (zero balances are omitted by the bank module); a misspelled or differently-formatted denom (uatom vs atom, factory/ibc denom prefixes); the correct denom but an unfunded wallet.

Common situations: Test setup forgets to fund the account; faucet mint hasn't landed; base vs display denomination confusion on testnets; address copied for the wrong network.

Related errors


AI-assisted analysis of unionlabs/union@031785bb6d (2026-08-16). Data as JSON: /api/errors/3c56c726dbbf9c95. Report an issue: GitHub.