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.
- exfer-0.10.0/.gitignore +44 -0
- exfer-0.10.0/CHANGELOG.md +246 -0
- exfer-0.10.0/LICENSE +21 -0
- exfer-0.10.0/PKG-INFO +168 -0
- exfer-0.10.0/README.md +136 -0
- exfer-0.10.0/pyproject.toml +93 -0
- exfer-0.10.0/src/exfer/__init__.py +67 -0
- exfer-0.10.0/src/exfer/_params.py +157 -0
- exfer-0.10.0/src/exfer/_transport.py +271 -0
- exfer-0.10.0/src/exfer/_version.py +1 -0
- exfer-0.10.0/src/exfer/async_client.py +744 -0
- exfer-0.10.0/src/exfer/client.py +916 -0
- exfer-0.10.0/src/exfer/errors.py +275 -0
- exfer-0.10.0/src/exfer/py.typed +0 -0
- exfer-0.10.0/src/exfer/types.py +524 -0
- exfer-0.10.0/tests/__init__.py +0 -0
- exfer-0.10.0/tests/conftest.py +54 -0
- exfer-0.10.0/tests/integration/__init__.py +0 -0
- exfer-0.10.0/tests/integration/conftest.py +223 -0
- exfer-0.10.0/tests/integration/test_roundtrip.py +67 -0
- exfer-0.10.0/tests/integration/test_tls.py +120 -0
- exfer-0.10.0/tests/test_client_async.py +360 -0
- exfer-0.10.0/tests/test_client_sync.py +493 -0
- exfer-0.10.0/tests/test_errors.py +204 -0
- exfer-0.10.0/tests/test_fingerprint.py +229 -0
- exfer-0.10.0/tests/test_transport.py +135 -0
- exfer-0.10.0/tests/test_v1_9_async.py +485 -0
- exfer-0.10.0/tests/test_v1_9_sync.py +577 -0
exfer-0.10.0/.gitignore
ADDED
|
@@ -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.
|