Skip to content

feat(gl-sdk): on-chain send preview, balance state, and fee rates#712

Open
angelix wants to merge 1 commit intomainfrom
ave-onchain-balance
Open

feat(gl-sdk): on-chain send preview, balance state, and fee rates#712
angelix wants to merge 1 commit intomainfrom
ave-onchain-balance

Conversation

@angelix
Copy link
Copy Markdown
Contributor

@angelix angelix commented Apr 30, 2026

Summary

Adds a fee-preview path for on-chain sends, a UI-ready classification of the on-chain wallet for the withdraw entry-point, and a fee-rates API that prevents below-relay submissions.

  • prepare_onchain_send(destination, amount_or_all, sat_per_vbyte) — runs CLN's coin selection at the given fee rate via fund_psbt(reserve=0) and returns the chosen UTXOs, fee, and post-fee recipient amount. No reservations, no discard step. Validates sat_per_vbyte against the node's min_acceptable relay floor and rejects below-floor rates with Error::Argument instead of letting the user discover the failure post-broadcast.
  • onchain_send gains optional sat_per_vbyte and utxos parameters. Passing the prepared utxos and sat_per_vbyte back reproduces the previewed fee bit-for-bit (CLN's Withdraw honors explicit inputs).
  • Node::onchain_balance_state() classifies the on-chain wallet into Unavailable / Available / ReserveOnly / PendingConfirmation / Immature for the withdraw entry-point. Runs list_funds, list_peer_channels, and a non-locking fund_psbt(reserve=0, satoshi=All) probe in parallel, deriving the actual emergency-reserve carving from total_inputs − excess − fee rather than guessing from channel presence. This is the single source of truth for the reserve; NodeState does not surface a separate field for it.
  • Node::onchain_fee_rates() returns OnchainFeeRates with five sat/vbyte buckets at confirmation targets (next_block_sat_per_vbyte, half_hour_sat_per_vbyte, hour_sat_per_vbyte, day_sat_per_vbyte, minimum_relay_sat_per_vbyte) sourced from CLN's feerates RPC — no third-party HTTP, no privacy leak. Field names carry units for consistency with the other on-chain types on this branch.

Correctness vs. Core Lightning source

Cross-checked against lightning/wallet/reservation.c, lightning/common/amount.c, lightning/bitcoin/tx.c:

  • Fee math: fee_sat = weight × feerate_per_kw / 1000 matches amount_tx_fee in common/amount.c:704.
  • startweight includes both the destination output weight (computed via bitcoin::Address::from_str(...).script_pubkey().len()) and the 42-wu base tx core overhead per bitcoin_tx_core_weight(1, 1). Without this we would have undercounted the fee by ~10–1000 sats depending on rate.
  • Inputs are extracted from the returned PSBT, since CLN only emits the reservations array when reserve > 0 (see reservation.c:421). Reading reservations with reserve=0 would silently return an empty list and break the pin-and-resend round-trip.
  • total_input_sat is summed directly from PSBT inputs to remain correct on sweeps where CLN's change_for_emergency (reservation.c:443) carves an emergency-reserve change output even on satoshi=All.

Dependencies

  • Adds bitcoin = { version = \"0.32\", features = [\"base64\"] }. The crate is already in the workspace lockfile transitively via gl-client; the base64 feature is required for Psbt::from_str.

Documentation

  • prepare_onchain_send doc comment directs callers to use its recipient_sat for "Send Max" UIs (the only authoritative post-fee amount).
  • OnchainBalanceState::Available.withdrawable_sat is documented as the entry-point label number.
  • Node::onchain_fee_rates returns confirmation-target buckets for fee picker UIs; minimum_relay_sat_per_vbyte is the relay-floor lower bound.

Bindings

Python bindings regenerated (libs/gl-sdk/glsdk/glsdk.py). Other language bindings need regeneration via task sdk:bindings-{kotlin,swift,ruby}.

Test plan

  • cargo build -p gl-sdk — clean
  • cargo test -p gl-sdk --lib — 35 unit tests pass
    • amount parsing (parse_amount_or_all_handles_all_variants)
    • output-weight derivation (output_weight_for_address_per_script_type)
    • balance-state classification across real-wallet scenarios (8 classify_onchain_balance_* cases including captured live-wallet data)
    • fee-rates bucket mapping (5 cases covering missing perkw, missing estimates, clamp-to-minimum, target-above-all-estimates)
  • cargo clippy -p gl-sdk — clean on new code
  • Python bindings regenerate cleanly (task sdk:bindings-python)
  • End-to-end test against a regtest node via gl-testing: prepare → send round-trip on partial and sweep with anchor channels, verify fee match and emergency-reserve carving
  • Smoke test the new types from Kotlin/Swift after binding regeneration

@cdecker
Copy link
Copy Markdown
Collaborator

cdecker commented Apr 30, 2026

Very nice changes 🤗

Just one call site that is missing the new Option<> arguments I think.

@angelix angelix force-pushed the ave-onchain-balance branch 3 times, most recently from 65eafff to ab6b399 Compare April 30, 2026 16:49
Adds a fee-preview path for on-chain sends so wallet UIs can show the
exact fee and recipient amount before broadcasting, with guarantees
that the broadcast tx matches the preview. Also adds a UI-ready
classification of the on-chain wallet for the withdraw entry-point,
and a fee-rates API that prevents below-relay submissions.

* `prepare_onchain_send(destination, amount_or_all, sat_per_vbyte)`
  returns the UTXOs CLN would select, the fee, and what the recipient
  would receive. Uses `fund_psbt` with `reserve=0` so nothing is locked.
  Validates `sat_per_vbyte` against the node's `min_acceptable` relay
  floor and rejects below-floor rates with `Error::Argument` instead
  of letting the user discover the failure post-broadcast.
* `onchain_send` gains optional `sat_per_vbyte` and `utxos` parameters.
  Passing the prepared `utxos` and `sat_per_vbyte` back reproduces the
  previewed fee bit-for-bit (CLN's `Withdraw` honors explicit inputs).
* `Node::onchain_balance_state()` classifies the on-chain wallet into
  Unavailable / Available / ReserveOnly / PendingConfirmation / Immature
  for the withdraw entry-point. Runs `list_funds`, `list_peer_channels`,
  and a non-locking `fund_psbt(reserve=0, satoshi=All)` probe in
  parallel, deriving the actual emergency-reserve carving from
  `total_inputs - excess - fee` rather than guessing from channel
  presence. This is the single source of truth for the reserve;
  `NodeState` does not surface a separate field for it.
* `Node::onchain_fee_rates()` returns `OnchainFeeRates` with five
  sat/vbyte buckets at confirmation targets (next_block, half_hour,
  hour, day, minimum_relay) sourced from CLN's `feerates` RPC. No
  third-party HTTP, no privacy leak. Field names carry units
  (`*_sat_per_vbyte`) for consistency with the other on-chain types
  on this branch.

Implementation cross-checked against CLN source
(`lightning/wallet/reservation.c`, `lightning/common/amount.c`,
`lightning/bitcoin/tx.c`):
* Fee math `weight × feerate_per_kw / 1000` matches CLN exactly.
* `startweight` includes both the destination output and the 42-wu
  base tx core overhead per `bitcoin_tx_core_weight(1, 1)`.
* Inputs are extracted from the returned PSBT, since CLN only emits
  the `reservations` array when `reserve > 0`.
* `total_input_sat` is summed directly from PSBT inputs to remain
  correct on sweeps where CLN carves an emergency-reserve change
  output.

Adds `bitcoin = "0.32"` (with `base64` feature) for PSBT parsing and
proper address-based output weight computation. Includes 35 unit
tests covering amount parsing, output-weight derivation, balance-state
classification across real-wallet scenarios, and fee-rates bucket
mapping.
@angelix angelix force-pushed the ave-onchain-balance branch from ab6b399 to 572dd0b Compare April 30, 2026 16:53
@angelix angelix changed the title feat(gl-sdk): prepare_onchain_send and on-chain balance classification feat(gl-sdk): on-chain send preview, balance state, and fee rates Apr 30, 2026
@angelix
Copy link
Copy Markdown
Contributor Author

angelix commented Apr 30, 2026

Very nice changes 🤗

Just one call site that is missing the new Option<> arguments I think.

Should be fixed, also added onchain_fee_rates.

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.

2 participants