The build::* transaction builders in l1/wallet panic via assert! when the chosen funding UTXO can't cover the outputs plus the node's minimum mass-based fee:
build::signed_carrier_transaction — amount > extra_value, amount > extra_value + fee
build::pay_to_address_transaction — amount > payout, amount > payout + fee
build::activity_transaction — amount > fee
build::settlement_transaction — fee_entry.amount > fee
These are pub and used in production through Wallet, which selects the UTXO to fund from (build_signed_carrier/build_covenant_bootstrap_transaction take utxos.into_iter().next(); pay_to_address the largest). A UTXO smaller than the required outputs+fee is a reachable runtime condition, not a programmer error, so it should surface as a recoverable error rather than crash the issuer.
Proposed change
- Add a
thiserror BuildError (e.g. InsufficientFunds { available, required }) in l1/wallet/src/build.rs; have the builders above return Result<Transaction, BuildError>. Fold the zero-fee probe's underflow guard into a single post-probe sufficiency check by probing the change output with a placeholder value (its value doesn't affect the byte layout / mass), the way activity_transaction already does.
- Propagate through the
Wallet methods (build_payload_transactions, build_subnet_payload_transactions, build_signed_carrier, build_covenant_bootstrap_transaction, prepare_settlement_transaction/_excluding, pay_to_address, build_activity_excluding) via a WalletError folding RpcError + BuildError + a no-spendable-UTXO case; the fetch .expect(...)s become ?.
- Update callers (settler,
examples/tn10-flow, sim driver, node/test-utils, e2e tests).
build::covenant_bootstrap_transaction does no fee math and has no funding-insufficiency path, so its builder stays infallible; only its Wallet wrapper gains the UTXO-selection error.
The
build::*transaction builders inl1/walletpanic viaassert!when the chosen funding UTXO can't cover the outputs plus the node's minimum mass-based fee:build::signed_carrier_transaction—amount > extra_value,amount > extra_value + feebuild::pay_to_address_transaction—amount > payout,amount > payout + feebuild::activity_transaction—amount > feebuild::settlement_transaction—fee_entry.amount > feeThese are
puband used in production throughWallet, which selects the UTXO to fund from (build_signed_carrier/build_covenant_bootstrap_transactiontakeutxos.into_iter().next();pay_to_addressthe largest). A UTXO smaller than the required outputs+fee is a reachable runtime condition, not a programmer error, so it should surface as a recoverable error rather than crash the issuer.Proposed change
thiserrorBuildError(e.g.InsufficientFunds { available, required }) inl1/wallet/src/build.rs; have the builders above returnResult<Transaction, BuildError>. Fold the zero-fee probe's underflow guard into a single post-probe sufficiency check by probing the change output with a placeholder value (its value doesn't affect the byte layout / mass), the wayactivity_transactionalready does.Walletmethods (build_payload_transactions,build_subnet_payload_transactions,build_signed_carrier,build_covenant_bootstrap_transaction,prepare_settlement_transaction/_excluding,pay_to_address,build_activity_excluding) via aWalletErrorfoldingRpcError+BuildError+ a no-spendable-UTXO case; the fetch.expect(...)s become?.examples/tn10-flow,simdriver,node/test-utils, e2e tests).build::covenant_bootstrap_transactiondoes no fee math and has no funding-insufficiency path, so its builder stays infallible; only itsWalletwrapper gains the UTXO-selection error.