Skip to content

Expose authenticated Esplora in the bindings#163

Open
philippem wants to merge 7 commits into
Blockstream:masterfrom
philippem:bindings/authenticated-esplora
Open

Expose authenticated Esplora in the bindings#163
philippem wants to merge 7 commits into
Blockstream:masterfrom
philippem:bindings/authenticated-esplora

Conversation

@philippem
Copy link
Copy Markdown

Expose authenticated Esplora in the bindings

Motivation

lwk_wollet already supports authenticated Esplora/Waterfalls backends (OAuth2, static token, custom headers), but the UniFFI bindings didn't expose any of it — so Python/Kotlin/Swift/C#/Go consumers couldn't connect to authenticated Blockstream Enterprise endpoints. While wiring this up, the work also surfaced a robustness gap: the Esplora client reported authenticated failures (e.g. 402 Insufficient credits) as a misleading invalid type: map, expected a sequence JSON error.

What's in here

  • bindings: authenticated Esplora access. New TokenProvider enum (None / Static / Blockstream OAuth2) plus headers and token_provider fields on EsploraClientBuilder, wired through to the core builder. Both fields are optional with defaults, so this is additive — existing EsploraClientBuilder(...) calls are unchanged.
  • wollet: surface HTTP errors. get_with_retry/post_with_retry now fail fast on any non-2xx status, returning the status code and response body instead of letting callers misparse an error payload. Behavioral change to existing methods (tip, full_scan, broadcast), but only in the error path.
  • docs + examples. A Python tab for the "Authenticated Esplora" book section, and runnable example/tests: an authenticated watch-only scan, an online-watcher/offline-signer flow, and confidential-change handling.

Validation

  • Rust: offline unit tests for the TokenProvider conversion and builder wiring; fmt + clippy clean.
  • Authenticated, live: ran a watch-only full scan of a real mainnet wallet through an authenticated enterprise endpoint — both the Esplora and Waterfalls code paths sync the same history (18 txs / 5 assets) and agree on balance; a negative path confirms invalid credentials are rejected. No private keys involved (watch-only).
  • Regtest: the watch-only and confidential-change examples run end-to-end against elementsd + Liquid electrs.

Reproducing the authenticated scan

Get OAuth2 API credentials from https://dashboard.blockstream.info/ (and ensure the account has credits), then:

CLIENT_ID=<your-client-id> CLIENT_SECRET=<your-client-secret> \
PYTHONPATH=$PWD/target/release/bindings \
python3 lwk_bindings/tests/bindings/authenticated_wallet_scan.py

Defaults target the production enterprise endpoint; override with ESPLORA_BASE_URL / ESPLORA_LOGIN_URL (e.g. for UAT). Without CLIENT_ID/CLIENT_SECRET the test logs a skip and exits 0.

API impact

No breaking changes. New additive binding API (TokenProvider + two optional builder fields); the only behavioral change is the Esplora client now erroring clearly on non-2xx responses.

philippem added 6 commits June 2, 2026 10:11
The async Esplora client returned the HTTP response for any non-retryable
status without checking it was successful, so callers parsed error bodies
as the expected success payload. An authenticated backend replying
`402 {"error":"Insufficient credits"}` therefore surfaced as a confusing
`invalid type: map, expected a sequence` JSON error instead of the actual
problem.

Fail fast in get_with_retry/post_with_retry on a non-success status,
returning the status code and a bounded prefix of the response body.
Expose the Esplora/Waterfalls authentication already available in
lwk_wollet through the UniFFI bindings, so Python/Kotlin/Swift/C#/Go
consumers can connect to enterprise backends.

- Add a TokenProvider enum (None/Static/Blockstream OAuth2) and map it
  to lwk_wollet::clients::TokenProvider
- Add headers and token_provider fields to EsploraClientBuilder and wire
  them through the From conversion
- Export TokenProvider from the crate root
- Add offline unit tests for the conversion and builder wiring
Add a Python tab to the "Authenticated Esplora" section of the clients
book page, backed by a runnable example that builds OAuth2, static-token,
and custom-header clients and asserts the builder wiring offline. When
CLIENT_ID/CLIENT_SECRET are set it also performs a live authenticated
request.
A non-trivial watch-only test that syncs a real mainnet wallet through an
authenticated Blockstream Enterprise endpoint and asserts on-chain facts
(tx history, per-asset balance == sum of UTXO values, confidential UTXOs).
No private keys are needed; it exercises both the Esplora and Waterfalls
code paths and checks they agree, plus a negative path asserting invalid
credentials are rejected.

Gated on CLIENT_ID/CLIENT_SECRET; defaults to the production enterprise
endpoint, overridable via ESPLORA_BASE_URL / ESPLORA_LOGIN_URL.
Two runnable regtest examples:

- watch_only_wallet.py: online watch-only wallet (descriptor only, no
  keys) paired with an offline signer; the watcher prepares and verifies
  an unsigned PSET, the signer signs, the watcher finalizes and broadcasts.
- liquid_change.py: demonstrates confidential change handling, identifying
  the internal-chain change output, unblinding it, and showing change lands
  on a fresh address on each spend.
fetch_oauth_token went straight to .json() and looked for "access_token",
so a failed token request (e.g. HTTP 401 {"error":"invalid_client"} for
bad credentials) was reported as a generic "Missing access_token in
response" that hid the real cause. Check the response status first and
surface it via error_for_status, e.g.:

  ... /token returned HTTP 401 Unauthorized: {"error":"invalid_client",
  "error_description":"Invalid client or Invalid client credentials"}

Caveat: this changes the error text on a rejected token request. Callers
that string-match the old "Missing access_token in response" message (only
emitted now when the endpoint returns 2xx without the field) may need to
update. No type or signature changes.
@philippem philippem requested review from LeoComandini and RCasatta and removed request for RCasatta June 2, 2026 17:51
Register the watch-only and confidential-change examples plus the
authenticated Esplora example with build_foreign_language_testcases! so
they run under `cargo test -p lwk_bindings --features foreign_bindings`.

The two regtest examples are hermetic (LwkTestEnv). The authenticated
example only builds clients and skips its live check unless CLIENT_ID /
CLIENT_SECRET are set, so it makes no external request in CI.
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