FuelLabs/fuel-core · error

required balances contain duplicate (asset, account) pair

Error message

required balances contain duplicate (asset, account) pair

What it means

The assembler requires the required_balances list to be unique per (asset_id, account owner) pair. It rejects the request when two entries reference the same asset for the same owner, because coin selection and change handling assume one balance requirement per pair.

Source

Thrown at crates/fuel-core/src/schema/tx/assemble_tx.rs:197

            return Err(anyhow::anyhow!(
                "The transaction has more inputs than allowed by the consensus"
            ));
        }

        if tx.outputs().len() > max_outputs as usize {
            return Err(anyhow::anyhow!(
                "The transaction has more outputs than allowed by the consensus"
            ));
        }

        if arguments.fee_index as usize >= arguments.required_balances.len() {
            return Err(anyhow::anyhow!("The fee address index is out of bounds"));
        }

        if has_duplicates(&arguments.required_balances, |balance| {
            (balance.asset_id, balance.account.owner())
        }) {
            return Err(anyhow::anyhow!(
                "required balances contain duplicate (asset, account) pair"
            ));
        }

        let base_asset_id = *arguments.consensus_parameters.base_asset_id();
        let mut signature_witness_indexes = HashMap::<Address, u16>::new();

        // Exclude inputs that already are used by the transaction
        let mut has_predicates = false;
        let inputs = tx.inputs();
        let mut unique_used_asset = HashSet::new();
        let mut set_contracts = HashSet::new();
        for input in inputs {
            if let Some(utxo_id) = input.utxo_id() {
                arguments.exclude.exclude(CoinId::Utxo(*utxo_id));
            }

            if let Some(nonce) = input.nonce() {

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Group and sum amounts client-side so each (assetId, owner) appears exactly once
  2. If the same owner needs balances for different assets, keep them as separate entries — only identical (asset, owner) pairs are rejected
  3. Use the same change policy for the merged entry to avoid also hitting the multiple-change-policies error

Example fix

// before
const requiredBalances = [
  { account: alice, assetId: BASE_ASSET, amount: 100 },
  { account: alice, assetId: BASE_ASSET, amount: 50 }, // duplicate pair
];

// after
const merged = new Map();
for (const b of rawBalances) {
  const k = `${b.account}:${b.assetId}`;
  const prev = merged.get(k) ?? { ...b, amount: 0 };
  prev.amount += b.amount;
  merged.set(k, prev);
}
const requiredBalances = [...merged.values()];
Defensive patterns

Strategy: validation

Validate before calling

function dedupeBalances<T extends { account: string; assetId: string; amount: bigint }>(balances: T[]): T[] {
  const out = new Map<string, T>();
  for (const b of balances) {
    const k = `${b.account}:${b.assetId}`;
    const prev = out.get(k);
    if (prev) prev.amount += b.amount;
    else out.set(k, { ...b });
  }
  return [...out.values()];
}

Type guard

function hasUniquePairs(balances: { account: string; assetId: string }[]): boolean {
  return new Set(balances.map((b) => `${b.account}:${b.assetId}`)).size === balances.length;
}

Try / catch

catch (e) { if (/duplicate \(asset, account\) pair/.test(e.message)) { requiredBalances = dedupeBalances(requiredBalances); /* retry */ } else throw e; }

Prevention

When it happens

Trigger: Passing two requiredBalances entries with the same assetId and the same account owner (different amount or change policy does not matter — the (asset, owner) key collides).

Common situations: Building the balances list in a loop over transfers without grouping by (asset, account); merging two wallet configurations that both add the base asset for the fee account; adding the fee payer separately when it is already present.

Related errors


AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16). Data as JSON: /api/errors/269e81a84424615f. Report an issue: GitHub.