Skip to content

fix: read at the head by default, add a global --finalized, add send --all - #169

Open
n13 wants to merge 1 commit into
mainfrom
n13/fee-estimate-at-head-and-transfer-all
Open

n13 wants to merge 1 commit into
mainfrom
n13/fee-estimate-at-head-and-transfer-all

Conversation

@n13

@n13 n13 commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Found live, minutes after mainnet enacted spec 153. Sending 1.99 QTC from an account holding exactly 2 QTC was refused:

Insufficient balance for send. Have: 2 QTC, Need: 2.000167025 QTC
(estimated fee: 0.010167025 QTC)

The transfer was affordable. Only the quote was stale.

Cause

partial_fee_estimate calls TransactionPaymentApi_query_info at latest_finalized_block_ref (subxt tx_client.rs:583). QPoW finality trails the head, so the quote came from the runtime the upgrade had just replaced — and 153 cut FEE_SCALE tenfold. Measured on mainnet at the time:

best: 97174   finalized: 97073   lag: 101 blocks
runtime @ head:      spec 153   ← fees 10x cheaper
runtime @ finalized: spec 152   ← what the estimate used

Real fee ~0.001 QTC; quoted 0.0102.

1. Estimate at the block everything else reads

QuantusClient::partial_fee runs the same runtime API at get_latest_block(). Replaces both partial_fee_estimate call sites (send.rs, cold_signing.rs).

2. One switch for finality

An audit of every finalized reference found those two sites were the only wrong ones — everything else is a deliberate --finalized-tx opt-in, or already fixed by #152 (collect-rewards proofs) and #154 (client metadata). So rather than add a parallel mechanism, this unifies what exists.

ExecutionMode::finalized already decided how long to wait for a transaction. It now also decides which block reads are taken at. Reads happen in ~80 places with no reason to carry an ExecutionMode, so main publishes the flag once via ExecutionMode::install, and get_latest_block consults it — one switch governs waits and reads, the way wait_for_transaction already governs waiting.

That makes wormhole_tip_block's own head/finalized branch redundant, and at_finalized_block unreachable; both go. --finalized-tx becomes --finalized (kept as a clap alias) since it no longer governs only transactions.

Default is the head. --finalized opts back in for callers who want finality's guarantees and will wait ~20 minutes for them.

3. send --all

Sweeping by subtracting an estimate cannot be made reliable — too low strands dust, too high is refused, which is exactly what happened above. --all submits Balances::transfer_all and lets the chain deduct the exact fee. --keep-alive chooses between reaping the account and leaving the existential deposit.

quantus send --from m1 --to <dest> --all

--amount and --all are mutually exclusive, enforced by clap.

Verification

  • 367 tests pass (3 new): the finality switch flips reads, --all builds Balances::transfer_all, and a fixed amount still builds transfer_allow_death
  • SKIP_CIRCUIT_BUILD=1 cargo clippy --all-targets --locked -- -D warnings clean
  • cargo +nightly fmt --all -- --check clean
  • Exercised against live mainnet on 153

Follow-up, not in this PR

COMPATIBLE_RUNTIMES still tops out at 149, so every mainnet command prints specVersion=153 … newer than this CLI's tested list. Cosmetic — transaction_version never moved off 6 — but all three chains are on 153 now and it should be bumped.

…--all

Sending 1.99 QTC from an account holding exactly 2 QTC was refused right after
mainnet enacted spec 153:

  Insufficient balance for send. Have: 2 QTC, Need: 2.000167025 QTC
  (estimated fee: 0.010167025 QTC)

subxt's partial_fee_estimate calls TransactionPaymentApi_query_info at
latest_finalized_block_ref. QPoW finality trails the head by ~100 blocks, so the
estimate was computed by spec 152 -- the runtime the upgrade had just replaced --
and 153 cut FEE_SCALE tenfold. The real fee was ~0.001 QTC. The transfer was
affordable; only the quote was stale.

Estimate against the same block as every other read, via
QuantusClient::partial_fee.

Unify the finality choice. ExecutionMode::finalized already decided how long to
wait for a transaction; it now also decides which block reads are taken at.
Reads happen in ~80 places that have no reason to carry an ExecutionMode, so
main publishes the flag once with ExecutionMode::install and
QuantusClient::get_latest_block consults it -- one switch for waits and reads.
wormhole_tip_block's own head/finalized branch and at_finalized_block go away,
since get_latest_block already answers that question.

--finalized-tx becomes --finalized (kept as an alias) because it no longer only
governs transactions.

Add send --all, which submits Balances::transfer_all and lets the chain deduct
the exact fee. Sweeping an account by subtracting an estimate cannot be done
reliably -- too low strands dust, too high is refused -- and --keep-alive
chooses between reaping the account and leaving the existential deposit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant