Preprod: Custom error 173 + "Wallet.InsufficientFunds: could not balance dust" despite large dust.balance(now) on shared bridge claimer

What are we trying to do?

Build a production bridge / capacity-exchange service on Midnight Preprod that uses a shared bridge claimer wallet to submit Ethereum- and BNB-originated claim transactions on Midnight (via @midnight-ntwrk/midnight-js ethereum-bridge-claim). The same claimer seed is used by three scripts:

  • register-bridge-claimer-dust.ts — registers unshielded coins for dust generation
  • check-bridge-claimer-dust-balance.ts — read-only dust snapshot
  • live claim submit path: submitEthereumClaimReal → sdkSubmitEthereumClaim → buildEthereumSdkProviders → getEthereumBridgeClaimerSeed() + initFacadeWallet

Claimer address (shared between all three paths):
mn_addr_preprod1yr0f5cqps3rmtfarhtkrrhqka5slrufax46uy3w333e7y63xarqsuddncn

What’s not working or unclear?

Two related errors, both on Preprod:

1. Dust-registration submit fails with Custom error 173

RPC-CORE: submitAndWatchExtrinsic(extrinsic: Extrinsic): ExtrinsicStatus:: 1010: Invalid Transaction: Custom error: 173
[info] Dust submit returned non-fatal node response: Transaction submission error

I’ve seen this same error in the #dev-chat thread “If a wallet has sufficient DUST balance” (by Cadalt, 04-04-2026). In that thread norm explained the related 170 error is caused by a 1–3h gap between root pruning in root_history (~1h) and OutOfDustValidityWindow kicking in (~3h). It’s unclear whether 173 is in the same family, a different check, or something else — please confirm.

2. Live claim submit fails with Wallet.InsufficientFunds: could not balance dust

After facade.waitForSyncedState() + dust.waitForSyncedState() complete, a read-only snapshot of the same claimer shows:

  • usable_dust_balance_specks: 9225916793000000000 (large, non-zero)
  • Unshielded summary: {"totalCoins":2,"registeredCount":2,"unregisteredCount":0,"unregisteredValueSum":"0"}

A separate run of register-bridge-claimer-dust.ts after long sync confirmed two coins both registered: totalCoins: 2, registeredCount: 2, unregisteredCount: 0.

Yet a live BNB claim submit against this same wallet fails with:

Wallet.InsufficientFunds: could not balance dust

Originating from @midnight-ntwrk/wallet-sdk-dust-wallet / the transacting path used when balancing the scoped claim transaction.

Live BNB smoke evidence:

  • Deposit id: bnb_7eb008c69866af1fdfb63c4b643459d40ceadc30e3c9c7b2caf01b955e1963a9_21
  • EVM tx hash: 0x7eb008c69866af1fdfb63c4b643459d40ceadc30e3c9c7b2caf01b955e1963a9
  • Observed state: CREDIT_READY; process reaches real Midnight ethereum-bridge-claim submission before failing.

What have you tried so far?

  • Confirmed the seed is identical between registration, read-only balance check, and submit (same getEthereumBridgeClaimerSeed() + initFacadeWallet everywhere).
  • Resolved a prior INVALID_SIGNATURE BNB attestation issue (witness/message alignment) — not the current failure.
  • Ruled out “no dust at all” — dust.balance(now) shows a large value.
  • Let dust.waitForSyncedState() run for several minutes on Preprod and re-checked — dust is visible and registered.
  • Read the #dev-chat Discord discussion where norm (Midnight mod) and Facu | Midnames advised on the 170 variant: “keep the wallet synced and submit fast. If the wallet sat idle between building the proof and submitting, that’s usually the cause.” I will try tightening our submit-after-proof window, but our service is a long-lived shared claimer that must handle bursts of claims, so a “submit within seconds of sync” pattern is fragile for production.
  • Verified the SDK-known isStrictlyComplete() hang on idle chains; using index check (appliedIndex >= highestRelevantWalletIndex) as backup.

Any relevant code or error messages?

Key error, verbatim, from the POST .../process call:

Wallet.InsufficientFunds: could not balance dust

Key error from dust-registration submit:

1010: Invalid Transaction: Custom error: 173
[info] Dust submit returned non-fatal node response: Transaction submission error

Full evidence packages attached:

  • MIDNIGHT_DUST_USABILITY_BLOCKER.md
  • 173-PREPROD-SUPPORT-POST.md

Questions for the Midnight team

  1. Is Custom error 173 in the 1010 extrinsic family the same root-pruning / OutOfDustValidityWindow cause as 170, or a different check? What does 173 specifically indicate?
  2. How should operators reconcile a large dust.balance(now) snapshot with a Wallet.InsufficientFunds at balancing on the same wallet, same moment, same seed? What is the dust-wallet balancer actually checking that dust.balance(now) is not?
  3. Is there a recommended pattern for a long-lived, always-on shared bridge claimer on Preprod/Mainnet — one that doesn’t rely on “build proof and submit within seconds”? E.g. a sync-before-each-submit pattern, a keepalive pattern, or a scoped-transaction rebuild on every attempt?
  4. Will the upcoming V5 indexer (server-side viewing-key scan) or the larger-proof allowance mentioned in #dev-chat change any of the above?

Where are you asking from?

  • Network: Preprod
  • SDKs: @midnight-ntwrk/midnight-js, @midnight-ntwrk/wallet-sdk-dust-wallet (exact versions in attached files)
  • Proof server: self-hosted, docker run -p 6300:6300 midnightntwrk/proof-server:latest
  • Service: shared bridge claimer for Ethereum + BNB, capacity-exchange rollout currently paused pending this clarification

Happy to provide more logs, a minimal repro, or a private session if useful.

Custom error 173 is InsufficientDustForRegistrationFee. It’s in the dust error family (169-173) but is not the same root-pruning cause as 170 or 171. The registration’s allow_fee_payment amount exceeded the available unclaimed funds for that key in the UTXO state. Check that your claimer has enough dust generated before the registration call, not just tNIGHT.

Full dust error family for reference:

  • 169 = InvalidDustRegistrationSignature
  • 170 = InvalidDustSpendProof (the root-pruning one)
  • 171 = OutOfDustValidityWindow (the 1-3h gap one)
  • 172 = MultipleDustRegistrationsForKey
  • 173 = InsufficientDustForRegistrationFee

On the InsufficientFunds despite large balance: dust.balance(now) reports total dust value but the balancer needs to construct spend proofs against specific UTXOs whose merkle roots must still be in root_history. If the wallet sat idle, those roots get pruned (~1h), so the balance reads fine but nothing is actually spendable. A resync rebuilds the wallet’s view against current roots.

For a long-lived shared claimer, one approach suggested in dev-chat was resync-before-each-submit: save wallet state to disk, resync the delta before each claim, then submit immediately. That said, there’s no established pattern for production bridge claimers yet. Flagging this to the team for guidance on recommended architecture.

On V5 indexer and larger proof allowance — don’t have visibility on whether those address these specific issues. Routing that to the team as well.

Sources: