{"record":{"id":"269e81a84424615f","repo":"FuelLabs/fuel-core","slug":"required-balances-contain-duplicate-asset-accoun","errorCode":null,"errorMessage":"required balances contain duplicate (asset, account) pair","messagePattern":"required balances contain duplicate \\(asset, account\\) pair","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/fuel-core/src/schema/tx/assemble_tx.rs","lineNumber":197,"sourceCode":"            return Err(anyhow::anyhow!(\n                \"The transaction has more inputs than allowed by the consensus\"\n            ));\n        }\n\n        if tx.outputs().len() > max_outputs as usize {\n            return Err(anyhow::anyhow!(\n                \"The transaction has more outputs than allowed by the consensus\"\n            ));\n        }\n\n        if arguments.fee_index as usize >= arguments.required_balances.len() {\n            return Err(anyhow::anyhow!(\"The fee address index is out of bounds\"));\n        }\n\n        if has_duplicates(&arguments.required_balances, |balance| {\n            (balance.asset_id, balance.account.owner())\n        }) {\n            return Err(anyhow::anyhow!(\n                \"required balances contain duplicate (asset, account) pair\"\n            ));\n        }\n\n        let base_asset_id = *arguments.consensus_parameters.base_asset_id();\n        let mut signature_witness_indexes = HashMap::<Address, u16>::new();\n\n        // Exclude inputs that already are used by the transaction\n        let mut has_predicates = false;\n        let inputs = tx.inputs();\n        let mut unique_used_asset = HashSet::new();\n        let mut set_contracts = HashSet::new();\n        for input in inputs {\n            if let Some(utxo_id) = input.utxo_id() {\n                arguments.exclude.exclude(CoinId::Utxo(*utxo_id));\n            }\n\n            if let Some(nonce) = input.nonce() {","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/FuelLabs/fuel-core/blob/b9d4d170da3a31c9ace5f963d633b326348e0d42/crates/fuel-core/src/schema/tx/assemble_tx.rs#L179-L215","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Group and sum amounts client-side so each (assetId, owner) appears exactly once","If the same owner needs balances for different assets, keep them as separate entries — only identical (asset, owner) pairs are rejected","Use the same change policy for the merged entry to avoid also hitting the multiple-change-policies error"],"exampleFix":"// before\nconst requiredBalances = [\n  { account: alice, assetId: BASE_ASSET, amount: 100 },\n  { account: alice, assetId: BASE_ASSET, amount: 50 }, // duplicate pair\n];\n\n// after\nconst merged = new Map();\nfor (const b of rawBalances) {\n  const k = `${b.account}:${b.assetId}`;\n  const prev = merged.get(k) ?? { ...b, amount: 0 };\n  prev.amount += b.amount;\n  merged.set(k, prev);\n}\nconst requiredBalances = [...merged.values()];","handlingStrategy":"validation","validationCode":"function dedupeBalances<T extends { account: string; assetId: string; amount: bigint }>(balances: T[]): T[] {\n  const out = new Map<string, T>();\n  for (const b of balances) {\n    const k = `${b.account}:${b.assetId}`;\n    const prev = out.get(k);\n    if (prev) prev.amount += b.amount;\n    else out.set(k, { ...b });\n  }\n  return [...out.values()];\n}","typeGuard":"function hasUniquePairs(balances: { account: string; assetId: string }[]): boolean {\n  return new Set(balances.map((b) => `${b.account}:${b.assetId}`)).size === balances.length;\n}","tryCatchPattern":"catch (e) { if (/duplicate \\(asset, account\\) pair/.test(e.message)) { requiredBalances = dedupeBalances(requiredBalances); /* retry */ } else throw e; }","preventionTips":["Always build requiredBalances through a grouping map keyed by (asset, account)","Remember only identical (asset, owner) pairs collide — different assets for the same account are fine","Merge change policies when merging duplicate entries"],"tags":["transaction","balances","validation","graphql","assemble-tx"],"backgroundTag":null,"analyzedSha":"b9d4d170da3a31c9ace5f963d633b326348e0d42","analyzedAt":"2026-08-16T08:56:42.692Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}