exfer 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+
22
+ # venvs
23
+ .venv/
24
+ venv/
25
+ env/
26
+
27
+ # tooling caches
28
+ .pytest_cache/
29
+ .mypy_cache/
30
+ .ruff_cache/
31
+ .coverage
32
+ htmlcov/
33
+ .tox/
34
+ .hypothesis/
35
+
36
+ # editor
37
+ .vscode/
38
+ .idea/
39
+ *.swp
40
+
41
+ # mdbook generated output
42
+ docs/book/
43
+
44
+ .claude/
@@ -0,0 +1,246 @@
1
+ # Changelog
2
+
3
+ ## 0.10.0 — 2026-06-10
4
+
5
+ ### Renamed — PyPI package `exfer-walletd` → `exfer`
6
+
7
+ The old distribution name collided with the Rust daemon: `pip install
8
+ exfer-walletd` looked like it installed the daemon, not this client.
9
+ Install and import change accordingly:
10
+
11
+ ```bash
12
+ pip install exfer
13
+ ```
14
+
15
+ ```python
16
+ from exfer import Client, AsyncClient
17
+ ```
18
+
19
+ No API changes — `0.10.0` is identical in surface to `exfer-walletd
20
+ 0.9.0`. The old `exfer-walletd` package on PyPI is discontinued; switch
21
+ to `exfer`.
22
+
23
+ ## 0.9.0 — 2026-06-09
24
+
25
+ ### Breaking — address methods now return full records
26
+
27
+ - `generate_address()` now returns a `GenerateAddressResult`
28
+ (`{"address", "pubkey", "index"}`) instead of a bare `str`. The
29
+ `pubkey` was previously dropped on the floor; it is the value to pass
30
+ as `payee_pubkey` to `quote_issue`, so callers no longer have to abuse
31
+ `sign_message` to recover it. Migrate `addr = c.generate_address()` →
32
+ `addr = c.generate_address()["address"]`.
33
+ - `list_addresses()` now returns `list[AddressRecord]`
34
+ (`{"address", "index"?, "label"?, "imported"?}`) instead of
35
+ `list[str]`, preserving the keystore index and label/imported flag.
36
+ Migrate `for a in c.list_addresses()` →
37
+ `for rec in c.list_addresses(): a = rec["address"]`.
38
+
39
+ New `TypedDict`s `GenerateAddressResult` and `AddressRecord` in
40
+ `exfer_walletd.types`. Mirrored on both `Client` and `AsyncClient`.
41
+
42
+ ### New methods
43
+
44
+ - `get_output_datum()` and `find_settlements_by_quote_id()` — the
45
+ EXFER-QUOTE settlement read surface (indexer-delegated on walletd).
46
+ - `simulate_transfer()` gained an optional `datum` argument, so a
47
+ settlement dry-run reflects the on-chain size of the datum it carries.
48
+
49
+ ### Fixes
50
+
51
+ - `transfer()` now sends the `outputs` array walletd expects; the old
52
+ flat `to`/`amount` shape was rejected by walletd.
53
+ - `wait_for_tx()` / `wait_for_payment()` extend the HTTP read timeout past
54
+ the server-side wait, so a long confirmation wait no longer surfaces as a
55
+ spurious "walletd unreachable" transport error.
56
+
57
+ ## 0.8.0 — walletd v1.9 + v1.9.1 surface
58
+
59
+ Adds the seventeen JSON-RPC methods walletd grew over its v1.7 → v1.9.1
60
+ series. Purely additive — every method that worked in 0.7.0 still
61
+ returns the same shape, with the same default behaviour, against the
62
+ same default port.
63
+
64
+ ### New methods (mirrored on `Client` + `AsyncClient`)
65
+
66
+ **HTLC spend trio (walletd v1.7+):**
67
+
68
+ - `htlc_lock(*, from_, receiver, hash_lock, timeout, amount, fee=, fee_rate=, max_fee=)`
69
+ - `htlc_claim(*, from_, lock_tx_id, preimage, sender, timeout, output_index=0, fee=)`
70
+ - `htlc_reclaim(*, from_, lock_tx_id, receiver, hash_lock, timeout, output_index=0, fee=)`
71
+
72
+ **Dry-run simulation (v1.9):**
73
+
74
+ - `simulate_transfer(*, from_, outputs, fee=, fee_rate=, max_fee=)`
75
+ - `simulate_htlc_lock(*, from_, receiver, hash_lock, timeout, amount, fee=, fee_rate=, max_fee=)`
76
+
77
+ Both methods compute the exact `(size, fee, fee_rate, ...)` a real call
78
+ would produce. No broadcast, no UTXO reservation. Lets an agent prove a
79
+ cost ceiling before committing to spend.
80
+
81
+ **Payment URI codec (v1.9, pure):**
82
+
83
+ - `payment_uri_encode(*, address, amount=, memo=, hash_lock=, timeout=, label=)`
84
+ - `payment_uri_decode(uri)`
85
+
86
+ BIP21-style `exfer:<address>?amount=...&memo=...` round trip.
87
+
88
+ **HTLC observability (v1.9, walletd's own index):**
89
+
90
+ - `htlc_status(lock_tx_id, output_index=0)`
91
+ - `htlc_list(*, role=, state=, since_height=, address=, limit=, cursor=)`
92
+ - `htlc_forget(lock_tx_id, output_index=0)`
93
+ - `get_follower_status()`
94
+ - `wait_for_tx(tx_id, *, min_confirmations=1, timeout_secs=60)`
95
+
96
+ `htlc_list` accepts either a single `HtlcState` or a list (untagged on
97
+ the wire, walletd handles both).
98
+
99
+ **Indexer-delegated (v1.9.1, multi-tenant queries):**
100
+
101
+ - `list_settlements(address, *, contract_hash=, since_height=, limit=, cursor=)`
102
+ - `contract_stats(address, *, contract_hash=)`
103
+ - `get_address_history(address, *, since_height=, limit=, cursor=)`
104
+ - `htlc_lookup_by_hashlock(hash_lock)`
105
+ - `get_output_spent_by(tx_id, output_index)`
106
+
107
+ These five methods need walletd to be running with `--indexer-rpc`
108
+ pointing at an `exfer-indexer` instance. When the flag isn't set the
109
+ SDK raises the new `IndexerNotConfiguredError`.
110
+
111
+ ### New result shapes in `exfer_walletd.types`
112
+
113
+ `HtlcRecord` / `HtlcParams` / `HtlcClaimRecord` / `HtlcReclaimRecord` /
114
+ `HtlcState` (Literal) / `HtlcRole` (Literal) / `HtlcLockResult` /
115
+ `HtlcClaimResult` / `HtlcReclaimResult` / `SimulateTransferResult` /
116
+ `SimulateHtlcLockResult` / `FollowerStatus` / `WaitForTxResult` /
117
+ `PaymentUri` / `SettlementRecord` / `ListSettlementsResult` /
118
+ `ContractStatsRow` / `AddressHistoryRow` / `AddressHistoryResult` /
119
+ `HtlcListResult` / `SpentByResult`.
120
+
121
+ ### New errors
122
+
123
+ - `WaitTimeoutError` (-32040) — `wait_for_tx` didn't see the depth in
124
+ time. Not terminal; the tx may still confirm.
125
+ - `IndexerNotConfiguredError` (-32041) — caller hit an indexer-delegated
126
+ method on a walletd without `--indexer-rpc` configured.
127
+
128
+ ### Compatibility
129
+
130
+ - Default URL / port unchanged (`http://127.0.0.1:7448`).
131
+ - All pre-existing methods preserve their signatures + return types.
132
+ - No new required runtime dependencies (`httpx` only, same as 0.7.0).
133
+
134
+ ## 0.7.0 — **breaking default**
135
+
136
+ - `Client.from_datadir()` / `AsyncClient.from_datadir()` default URL
137
+ port flipped from `:8080` to `:7448`, matching walletd v0.7.0's new
138
+ default `--bind`. If you set `--bind` on the walletd side, this is a
139
+ no-op. If you relied on `:8080` defaults end-to-end, restart walletd
140
+ with `--bind :8080` (or update your config to `:7448`).
141
+ - All examples in README + docs use `:7448`.
142
+
143
+ ## 0.6.0
144
+
145
+ - **TLS pinning** for walletd's new `--tls` mode (walletd v0.5.0+).
146
+ Construct with `Client(url="https://…", token="…",
147
+ fingerprint="sha256:…")` and the SDK installs a custom transport that
148
+ verifies the server's leaf cert by SHA-256 instead of CA chain.
149
+ Mismatches raise `FingerprintMismatchError` (a `TransportError`
150
+ subclass).
151
+ - `from_env()` gained `fingerprint_env="WALLETD_FINGERPRINT"` —
152
+ optional; only used when set.
153
+ - `from_datadir()` auto-reads `<datadir>/cert.fingerprint` whenever
154
+ `url` starts with `https://`. Plain http URLs ignore it entirely.
155
+ - `fingerprint=` and `transport=` are mutually exclusive (passing both
156
+ raises `ValueError`); passing `fingerprint=` with an `http://` URL
157
+ also raises (almost always a bug).
158
+ - Public re-exports: `FingerprintMismatchError`.
159
+
160
+ ## 0.5.0 — **breaking**
161
+
162
+ API polish based on dogfooding. None of the bytes-on-the-wire changed;
163
+ this is purely SDK shape.
164
+
165
+ ### Breaking changes
166
+
167
+ - **Single-value methods now return bare values**, not dicts:
168
+ - `generate_address() -> str` (was `{"address": ..., "pubkey": ...}`;
169
+ pubkey is dropped — open an issue if you need it back)
170
+ - `get_balance(addr) -> int` (was `{"address": ..., "balance": ...}`)
171
+ - `get_block_height() -> int` (was `{"height": ..., "block_id": ...}`)
172
+ - `send_raw_transaction(tx_hex) -> str` (was `{"tx_id": ...}`)
173
+ - **`get_block(*, height|hash)` split into two methods**:
174
+ - `get_block_by_height(height: int) -> Block`
175
+ - `get_block_by_hash(block_hash: str) -> Block`
176
+ - The keyword-variant raised `TypeError` at runtime when called wrong;
177
+ splitting kills the runtime check and gives IDEs full signal.
178
+ - **`get_transaction(tx_id=...)`** — parameter renamed from `hash` (the
179
+ builtin) to `tx_id` (the semantic name). Wire field stays `hash`.
180
+ - **`ping() -> None`** (was `{"ok": True}`). Success = "didn't raise".
181
+ - **`WalletdError.__str__`** now includes the code:
182
+ `"[-32020] upstream node unreachable"`. `e.code` and `e.message`
183
+ still exist as attributes.
184
+
185
+ ### Additions
186
+
187
+ - **`ExferError`** — common ancestor of `WalletdError` and
188
+ `TransportError`. Lets you `except ExferError` as a blanket SDK
189
+ catch (the two operational branches stay distinct underneath).
190
+ - **`get_tip() -> Tip`** — `NamedTuple(height: int, block_id: str)`
191
+ for the case where you want both pieces of the chain tip. Use
192
+ `get_block_height()` when you only need the int.
193
+ - **`InsufficientBalanceError.in_flight_reserved`** now prefers the
194
+ error envelope's `data` field over message-scraping, with the
195
+ message-scrape as a fallback. Forward-compatible with a planned
196
+ walletd change to surface this structurally.
197
+
198
+ ### Internal
199
+
200
+ - `TypedDict`s no longer re-exported from `exfer_walletd` top-level —
201
+ import from `exfer_walletd.types` if you want them. `Tip` stays
202
+ top-level because it's the actual return type of a method.
203
+ - `User-Agent` version no longer late-imported inside a helper
204
+ function.
205
+
206
+ ## 0.4.0
207
+
208
+ - mdBook docs site (`docs/`) deployed to GitHub Pages on every push to
209
+ main. Covers intro, install, quick-start, async usage, full API
210
+ reference, errors table, and FAQ. Local preview: `mdbook serve docs`.
211
+
212
+ ## 0.3.0
213
+
214
+ - `tests/integration/` spawns a real `exfer-walletd` binary in a temp
215
+ datadir with `--node-rpc` pointed at a closed port. Round-trips
216
+ `healthz`, `ping`, `generate_address`, `list_addresses`, plus auth
217
+ and upstream-error rejection. Skipped automatically if the binary
218
+ isn't built (`cargo build --release` in `../exfer-walletd`) or
219
+ `WALLETD_BINARY` env var isn't set.
220
+ - New CI job `integration` checks out `exfer-stack/exfer-walletd@v0.4.3`,
221
+ builds it, and runs the integration suite on `main` pushes.
222
+ - `InsufficientBalanceError.in_flight_reserved` now has a regression
223
+ test that reconstructs walletd's exact format string from
224
+ `src/error.rs::insufficient_balance_message` byte-for-byte — if
225
+ walletd ever rewords the message, the test catches it before users
226
+ hit a silently-wrong `False`.
227
+
228
+ ## 0.2.0
229
+
230
+ - `AsyncClient` mirrors every method on `Client`, on top of
231
+ `httpx.AsyncClient`. Sync and async share `_transport.py` so they
232
+ can't drift on the wire.
233
+
234
+ ## 0.1.0
235
+
236
+ Initial release.
237
+
238
+ - Sync `Client` covering every `exfer-walletd` JSON-RPC method:
239
+ `ping`, `generate_address`, `list_addresses`, `get_balance`,
240
+ `get_address_utxos`, `get_script_utxos`, `get_block_height`,
241
+ `get_block`, `get_transaction`, `transfer`, `send_raw_transaction`.
242
+ - Unauthenticated `healthz()` for liveness probes.
243
+ - Typed exception hierarchy mapped 1:1 to walletd's documented
244
+ JSON-RPC error codes.
245
+ - `TypedDict` return shapes for every result — zero runtime cost,
246
+ full mypy/pyright coverage.
exfer-0.10.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Exfer community contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
exfer-0.10.0/PKG-INFO ADDED
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: exfer
3
+ Version: 0.10.0
4
+ Summary: Typed Python client for the exfer-walletd JSON-RPC API
5
+ Project-URL: Homepage, https://github.com/exfer-stack/exfer-py
6
+ Project-URL: Repository, https://github.com/exfer-stack/exfer-py
7
+ Project-URL: Bug Tracker, https://github.com/exfer-stack/exfer-py/issues
8
+ Project-URL: exfer-walletd (Rust daemon), https://github.com/exfer-stack/exfer-walletd
9
+ Author: exfer-stack
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: crypto,exfer,json-rpc,wallet,walletd
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: <3.14,>=3.9
24
+ Requires-Dist: httpx<1.0,>=0.27
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy<2.0,>=1.10; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: respx>=0.21; extra == 'dev'
30
+ Requires-Dist: ruff>=0.5; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # exfer (Python SDK)
34
+
35
+ Typed Python client for the [`exfer-walletd`](https://github.com/exfer-stack/exfer-walletd)
36
+ JSON-RPC API.
37
+
38
+ ```bash
39
+ pip install exfer
40
+ ```
41
+
42
+ ```python
43
+ from exfer import Client
44
+
45
+ with Client("http://127.0.0.1:7448", token="...") as c:
46
+ assert c.healthz() # True
47
+ res = c.generate_address() # {address, pubkey, index}
48
+ addr = res["address"]
49
+ bal = c.get_balance(addr) # int (exfers)
50
+
51
+ tx = c.transfer(
52
+ from_="<your-managed-address>",
53
+ to="<recipient-address>",
54
+ amount=30_000_000, # exfers; 1 EXFER = 100_000_000 exfers
55
+ )
56
+ print(tx["tx_id"])
57
+ ```
58
+
59
+ ## What this is
60
+
61
+ - A thin, typed wrapper over walletd's JSON-RPC. One method per RPC method.
62
+ - Both sync (`Client`) and async (`AsyncClient`) — same surface, shared
63
+ wire layer so they can't drift.
64
+ - Single-value endpoints return bare `str` / `int`. Multi-field
65
+ endpoints return `TypedDict`s. No `pydantic` dependency.
66
+
67
+ ## Async
68
+
69
+ ```python
70
+ from exfer import AsyncClient
71
+
72
+ async with AsyncClient("http://127.0.0.1:7448", token) as c:
73
+ assert await c.healthz()
74
+ res = await c.generate_address() # {address, pubkey, index}
75
+ print(await c.get_balance(res["address"]))
76
+ ```
77
+
78
+ ## What this isn't
79
+
80
+ - **Not a chain client.** This SDK talks to walletd, which talks to a
81
+ node. It never holds keys, never signs transactions, never derives
82
+ addresses. If you want client-side signing, run walletd.
83
+ - Not a high-level wallet abstraction. Methods map 1:1 to the wire
84
+ grammar; build helpers on top as your application needs them.
85
+
86
+ ## Token discovery
87
+
88
+ ```python
89
+ # 1. Explicit
90
+ Client("http://127.0.0.1:7448", "your-token")
91
+
92
+ # 2. From env vars (deployed backends): WALLETD_URL + WALLETD_AUTH_TOKEN
93
+ Client.from_env()
94
+
95
+ # 3. From a local walletd datadir (dev / colocated)
96
+ Client.from_datadir() # reads ~/.exfer-walletd/token
97
+ ```
98
+
99
+ If walletd is configured with split scopes
100
+ (`WALLETD_AUTH_TOKEN_READ` + `WALLETD_AUTH_TOKEN_SPEND`), construct one
101
+ `Client` per scope. A read-only token calling `transfer` raises
102
+ `AuthenticationError`.
103
+
104
+ ## Errors
105
+
106
+ Every documented walletd error code maps to a typed exception, all
107
+ rooted at `ExferError`:
108
+
109
+ ```python
110
+ from exfer import (
111
+ ExferError, # blanket catch
112
+ AuthenticationError, # -32001
113
+ WalletNotFoundError, # -32010
114
+ UpstreamError, # -32020 — walletd's upstream node is unreachable
115
+ TxAuthError, # -32030 — UTXO authentication failed
116
+ InsufficientBalanceError, # -32031 — wallet can't cover amount+fee
117
+ InvalidParamsError, # -32602
118
+ TransportError, # walletd itself unreachable / non-JSON body
119
+ )
120
+ ```
121
+
122
+ `str(e)` is the `[-32xxx] message` form, so plain
123
+ `log.error(f"{e}")` is enough for production.
124
+
125
+ `InsufficientBalanceError.in_flight_reserved` is `True` when the
126
+ shortfall comes from UTXOs reserved by other pending transfers from the
127
+ same walletd — retry after they confirm.
128
+
129
+ ## TLS
130
+
131
+ When walletd is started with `--tls` (v0.5.0+), point the SDK at the
132
+ `https://` URL and supply the fingerprint walletd printed on first
133
+ run:
134
+
135
+ ```python
136
+ with Client.from_datadir(url="https://<walletd-host>:7448") as c:
137
+ c.ping() # auto-reads cert.fingerprint alongside token
138
+ ```
139
+
140
+ The SDK pins the leaf cert by SHA-256, bypassing the CA chain
141
+ entirely. Mismatches raise `FingerprintMismatchError`.
142
+
143
+ ## Docs
144
+
145
+ Full docs site: **<https://exfer-stack.github.io/exfer-py/>**.
146
+
147
+ ## Capabilities
148
+
149
+ One typed method per walletd RPC, covering: addresses, balances, blocks,
150
+ and transactions; `transfer` plus dry-run `simulate_transfer` /
151
+ `simulate_htlc_lock`; the HTLC spend trio
152
+ (`htlc_lock` / `htlc_claim` / `htlc_reclaim`) and HTLC observability
153
+ (`htlc_status` / `htlc_list` / `htlc_forget` / `htlc_lookup_by_hashlock`);
154
+ BIP21-style payment URIs (`payment_uri_encode` / `payment_uri_decode`);
155
+ EXFER-QUOTE signed price credentials (`quote_issue` / `quote_verify`);
156
+ message signing (`sign_message` / `verify_message`); indexer-delegated
157
+ queries (`list_settlements`, `contract_stats`, `get_address_history`,
158
+ `get_attestation_edges`, `get_output_spent_by`, `get_output_datum`,
159
+ `find_settlements_by_quote_id`, `detect_in_chain_swaps`); and
160
+ observability (`wait_for_tx`, `wait_for_payment`, `get_follower_status`).
161
+
162
+ ## Status
163
+
164
+ `0.10.0` — alpha. Tested against `exfer-walletd >= 1.9.1` (HTLC,
165
+ dry-run simulation, payment URIs, quotes, message signing, and
166
+ indexer-delegated queries). TLS pinning supported since `0.6.0`.
167
+
168
+ MIT licensed.
exfer-0.10.0/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # exfer (Python SDK)
2
+
3
+ Typed Python client for the [`exfer-walletd`](https://github.com/exfer-stack/exfer-walletd)
4
+ JSON-RPC API.
5
+
6
+ ```bash
7
+ pip install exfer
8
+ ```
9
+
10
+ ```python
11
+ from exfer import Client
12
+
13
+ with Client("http://127.0.0.1:7448", token="...") as c:
14
+ assert c.healthz() # True
15
+ res = c.generate_address() # {address, pubkey, index}
16
+ addr = res["address"]
17
+ bal = c.get_balance(addr) # int (exfers)
18
+
19
+ tx = c.transfer(
20
+ from_="<your-managed-address>",
21
+ to="<recipient-address>",
22
+ amount=30_000_000, # exfers; 1 EXFER = 100_000_000 exfers
23
+ )
24
+ print(tx["tx_id"])
25
+ ```
26
+
27
+ ## What this is
28
+
29
+ - A thin, typed wrapper over walletd's JSON-RPC. One method per RPC method.
30
+ - Both sync (`Client`) and async (`AsyncClient`) — same surface, shared
31
+ wire layer so they can't drift.
32
+ - Single-value endpoints return bare `str` / `int`. Multi-field
33
+ endpoints return `TypedDict`s. No `pydantic` dependency.
34
+
35
+ ## Async
36
+
37
+ ```python
38
+ from exfer import AsyncClient
39
+
40
+ async with AsyncClient("http://127.0.0.1:7448", token) as c:
41
+ assert await c.healthz()
42
+ res = await c.generate_address() # {address, pubkey, index}
43
+ print(await c.get_balance(res["address"]))
44
+ ```
45
+
46
+ ## What this isn't
47
+
48
+ - **Not a chain client.** This SDK talks to walletd, which talks to a
49
+ node. It never holds keys, never signs transactions, never derives
50
+ addresses. If you want client-side signing, run walletd.
51
+ - Not a high-level wallet abstraction. Methods map 1:1 to the wire
52
+ grammar; build helpers on top as your application needs them.
53
+
54
+ ## Token discovery
55
+
56
+ ```python
57
+ # 1. Explicit
58
+ Client("http://127.0.0.1:7448", "your-token")
59
+
60
+ # 2. From env vars (deployed backends): WALLETD_URL + WALLETD_AUTH_TOKEN
61
+ Client.from_env()
62
+
63
+ # 3. From a local walletd datadir (dev / colocated)
64
+ Client.from_datadir() # reads ~/.exfer-walletd/token
65
+ ```
66
+
67
+ If walletd is configured with split scopes
68
+ (`WALLETD_AUTH_TOKEN_READ` + `WALLETD_AUTH_TOKEN_SPEND`), construct one
69
+ `Client` per scope. A read-only token calling `transfer` raises
70
+ `AuthenticationError`.
71
+
72
+ ## Errors
73
+
74
+ Every documented walletd error code maps to a typed exception, all
75
+ rooted at `ExferError`:
76
+
77
+ ```python
78
+ from exfer import (
79
+ ExferError, # blanket catch
80
+ AuthenticationError, # -32001
81
+ WalletNotFoundError, # -32010
82
+ UpstreamError, # -32020 — walletd's upstream node is unreachable
83
+ TxAuthError, # -32030 — UTXO authentication failed
84
+ InsufficientBalanceError, # -32031 — wallet can't cover amount+fee
85
+ InvalidParamsError, # -32602
86
+ TransportError, # walletd itself unreachable / non-JSON body
87
+ )
88
+ ```
89
+
90
+ `str(e)` is the `[-32xxx] message` form, so plain
91
+ `log.error(f"{e}")` is enough for production.
92
+
93
+ `InsufficientBalanceError.in_flight_reserved` is `True` when the
94
+ shortfall comes from UTXOs reserved by other pending transfers from the
95
+ same walletd — retry after they confirm.
96
+
97
+ ## TLS
98
+
99
+ When walletd is started with `--tls` (v0.5.0+), point the SDK at the
100
+ `https://` URL and supply the fingerprint walletd printed on first
101
+ run:
102
+
103
+ ```python
104
+ with Client.from_datadir(url="https://<walletd-host>:7448") as c:
105
+ c.ping() # auto-reads cert.fingerprint alongside token
106
+ ```
107
+
108
+ The SDK pins the leaf cert by SHA-256, bypassing the CA chain
109
+ entirely. Mismatches raise `FingerprintMismatchError`.
110
+
111
+ ## Docs
112
+
113
+ Full docs site: **<https://exfer-stack.github.io/exfer-py/>**.
114
+
115
+ ## Capabilities
116
+
117
+ One typed method per walletd RPC, covering: addresses, balances, blocks,
118
+ and transactions; `transfer` plus dry-run `simulate_transfer` /
119
+ `simulate_htlc_lock`; the HTLC spend trio
120
+ (`htlc_lock` / `htlc_claim` / `htlc_reclaim`) and HTLC observability
121
+ (`htlc_status` / `htlc_list` / `htlc_forget` / `htlc_lookup_by_hashlock`);
122
+ BIP21-style payment URIs (`payment_uri_encode` / `payment_uri_decode`);
123
+ EXFER-QUOTE signed price credentials (`quote_issue` / `quote_verify`);
124
+ message signing (`sign_message` / `verify_message`); indexer-delegated
125
+ queries (`list_settlements`, `contract_stats`, `get_address_history`,
126
+ `get_attestation_edges`, `get_output_spent_by`, `get_output_datum`,
127
+ `find_settlements_by_quote_id`, `detect_in_chain_swaps`); and
128
+ observability (`wait_for_tx`, `wait_for_payment`, `get_follower_status`).
129
+
130
+ ## Status
131
+
132
+ `0.10.0` — alpha. Tested against `exfer-walletd >= 1.9.1` (HTLC,
133
+ dry-run simulation, payment URIs, quotes, message signing, and
134
+ indexer-delegated queries). TLS pinning supported since `0.6.0`.
135
+
136
+ MIT licensed.