API reference
Every method exists on both Client (sync) and AsyncClient (async)
with identical names, parameters, and return shapes. The async version
is async def and must be awaited; otherwise they're interchangeable.
Amounts are integers in exfers, where 1 EXFER = 100_000_000 exfers.
Hex strings (addresses, hashes, scripts, tx bytes) are lowercase, no
0x prefix.
Construction
Client(url: str, token: str, *, timeout: float = 30.0,
transport: httpx.BaseTransport | None = None,
fingerprint: str | None = None)
AsyncClient(url, token, *, timeout=30.0, transport=None, fingerprint=None)
fingerprint enables TLS pinning when walletd is run with --tls.
Format is "sha256:<lowercase-hex-64>" (the exact string walletd
writes to <datadir>/cert.fingerprint on first run). Requires an
https:// URL. The pinning transport replaces CA-chain validation —
walletd's leaf cert is trusted iff its SHA-256 matches.
transport= and fingerprint= are mutually exclusive — the latter
installs a pinning transport itself, and accepting a custom one
alongside would silently bypass verification.
Alternate constructors:
Client.from_env(*, url_env="WALLETD_URL",
token_env="WALLETD_AUTH_TOKEN",
fingerprint_env="WALLETD_FINGERPRINT")
Client.from_datadir(*, url="http://127.0.0.1:7448",
datadir="~/.exfer-walletd")
from_env reads WALLETD_FINGERPRINT if set (otherwise plaintext
HTTP). from_datadir auto-reads <datadir>/cert.fingerprint when
url is https://, raising FileNotFoundError if walletd hasn't
been started with --tls yet.
Raise RuntimeError (from_env) or FileNotFoundError
(from_datadir) if the required inputs are missing.
Liveness
healthz() -> bool
Probe GET /healthz — TCP+HTTP only. Returns True iff walletd
answered 200 OK with body ok. Returns False on any failure
rather than raising — drops cleanly into liveness loops.
No Authorization header is sent. A green healthz says nothing
about whether your token is valid or whether walletd's upstream node
is reachable.
ping() -> None
Authenticated JSON-RPC round-trip. Returns None on success and
raises on any failure. Use this when you want to verify the token is
valid and walletd's RPC layer is up — not just the TCP socket.
Read-scope methods
generate_address() -> GenerateAddressResult
Create a new managed address. Returns a GenerateAddressResult —
{"address", "pubkey", "index"} (address and pubkey are lowercase
64-char hex). The pubkey is the value to pass as payee_pubkey to
quote_issue. Walletd persists the keypair on disk under
<datadir>/wallets/<address>.key.
list_addresses() -> list[AddressRecord]
Every address walletd holds a key for, sorted ascending. Each
AddressRecord is {"address", "index", and one of "label"/"imported"}.
get_balance(address: str) -> int
Confirmed balance for address, in exfers. Mempool UTXOs are not
counted; for the mempool-aware view, use get_address_utxos.
get_address_utxos(address: str) -> UtxosResult
Confirmed UTXOs locked to address plus tip metadata:
{
"address": "27e1c8...",
"script_hex": None,
"tip_height": 577429,
"truncated": False,
"utxos": [
{
"tx_id": "a02ab0...",
"output_index": 1,
"value": 69900000,
"height": 577429,
"is_coinbase": False,
"script_len": None,
},
],
}
truncated is True if the upstream hit a result limit.
get_script_utxos(script_hex: str) -> UtxosResult
Same shape as get_address_utxos, but matches by raw locking script.
address is always None in the result.
get_block_height() -> int
Current chain tip height. For the (height, block_id) pair, use
get_tip().
get_tip() -> Tip
Current chain tip as a NamedTuple:
from exfer import Tip
tip = c.get_tip()
print(tip.height, tip.block_id)
h, b = tip # unpack works too
get_block_by_height(height: int) -> Block
{
"hash": "17b95f...",
"height": 577429,
"prev_block_id": "...",
"state_root": "...",
"tx_root": "...",
"timestamp": 1700000000,
"nonce": 42,
"difficulty_target": "...",
"tx_count": 1,
"transactions": ["a02ab0..."],
}
get_block_by_hash(block_hash: str) -> Block
Same shape; lookup by block hash instead of height.
get_transaction(tx_id: str) -> Transaction
Fetch a transaction. Covers mempool + confirmed; in_mempool
distinguishes.
{
"tx_id": "a02ab0...",
"tx_hex": "01000200...",
"in_mempool": False,
"block_hash": "1bac70...", # None if in mempool
"block_height": 577429, # None if in mempool
}
quote_verify(quote: QuoteJson) -> QuoteVerifyResult
Accept-check a signed EXFER-QUOTE. Pure and key-free (Read scope):
reconstructs the signing image against the live node genesis and clock,
then checks the signature, key validity, and TTL/skew/expiry windows.
quote is the signed quote object (the quote field of a quote_issue
result).
{
"valid": True,
"reason": "...", # present only when valid is False
"signer_address": "...", # derived from signer_pubkey — always present
"payee_address": "...", # derived from payee_pubkey — always present
"genesis_block_id": "...", # the live genesis checked against
}
Proves authorship, not authority — the acceptor still decides whether it trusts the signer.
Spend-scope methods
transfer(*, from_, to, amount, fee=None, datum=None) -> TransferResult
Build, sign, and broadcast a payment from a managed wallet.
from_(trailing underscore) maps to wire fieldfrom.amount,feeare integers in exfers.- Omitting
feelets walletd apply its default (100_000 = 0.001 EXFER). datum(hex, <= 4096 bytes) is an app-defined on-chain blob attached to the output; read it back viaget_transaction/get_output_datum.
{
"tx_id": "a02ab0...",
"size": 227,
"tip_height": 577427,
"submitted": True,
}
Common errors:
WalletNotFoundError— walletd doesn't hold the key forfrom_.InsufficientBalanceError— check.in_flight_reservedto decide whether to retry.UpstreamError— node rejected the broadcast or is unreachable.TxAuthError— UTXO authentication failed; the upstream may be malicious or out of sync.
See Errors for the full list.
send_raw_transaction(tx_hex: str) -> str
Broadcast a pre-signed transaction. Returns the broadcast tx_id.
Used by transfer internally; exposed for callers that build
transactions externally.
htlc_lock(*, from_, receiver, hash_lock, timeout, amount, fee=None, fee_rate=None, max_fee=None) -> HtlcLockResult
Lock amount exfers from from_ into a new HTLC output. receiver is
the receiver's 32-byte pubkey (64 hex); timeout is the absolute block
height past which from_ can reclaim. fee and fee_rate are mutually
exclusive (passing both raises ValueError); omit both to let walletd
pick its default rate.
{
"tx_id": "...",
"output_index": 0, # save this — every HTLC op needs it
"size": 231,
"fee": 100000,
"fee_rate": 1,
"amount": 30000000,
"change": 69900000,
"submitted": True,
"tip_height": 577429,
}
htlc_claim(*, from_, lock_tx_id, preimage, sender, timeout, output_index=0, fee=None) -> HtlcClaimResult
Spend an HTLC via the hash arm by revealing preimage (hex, any
length). from_ is the receiver wallet that claims the funds; sender
and timeout reconstruct the original locking script.
{
"tx_id": "...",
"size": 192,
"fee": 100000,
"amount": 29900000,
"submitted": True,
"tip_height": 577430,
}
htlc_reclaim(*, from_, lock_tx_id, receiver, hash_lock, timeout, output_index=0, fee=None) -> HtlcReclaimResult
Reclaim an expired HTLC via the timeout arm. from_ is the original
sender. Walletd checks the chain tip and refuses to broadcast while
current_height <= timeout. Same result shape as htlc_claim.
quote_issue(*, address, payee_pubkey, currency, amount_minor, rate_exfers_per_unit, exfer_amount, ttl_secs, payer_pubkey=None, memo=None, quote_id=None) -> QuoteIssueResult
Construct and sign an EXFER-QUOTE — a signed price credential (Spend scope; mints a credential).
addressis the issuer/signer wallet whose key signs the image.payee_pubkey(64 hex) is the party to be paid.currencyis a 3-12 char[A-Z0-9]pricing-unit code;exfer_amountis the only binding amount (exfers).ttl_secssets the lifetime (0 < ttl_secs <= 3600).payer_pubkey(64 hex), when given, binds the quote to a payer (non-transferable).memois a signed UTF-8 note (<= 256 bytes).quote_id(32 hex) pins the id; omitted means walletd generates 16 random bytes.
{
"quote": { ... }, # full signed QuoteJson to hand out
"signature": "...", # hex128
"signer_address": "...",
"payee_address": "...",
"genesis_block_id": "...",
"image": "...", # hex of the exact signed bytes, for audit
"htlc_preimage": "...", # hex64 — KEEP SECRET
"htlc_hash_lock": "...", # hex64, SHA-256(htlc_preimage)
}
Dry-run simulation
Read-scope. Both methods compute the exact (size, fee, fee_rate, ...)
a real call would produce — no broadcast, no UTXO reservation — so an
agent can prove a cost ceiling before committing to spend.
simulate_transfer(*, from_, outputs, fee=None, fee_rate=None, max_fee=None, datum=None) -> SimulateTransferResult
Dry-run version of transfer. outputs is a list of
{"to": addr_hex, "amount": exfers} (up to 16 entries). fee and
fee_rate are mutually exclusive. datum (hex, even length, <= 4096
bytes) sizes the simulated tx with the datum bytes — pass the quote_id
you intend to attach so the dry-run matches the real settlement transfer.
{
"size": 227,
"fee": 100000,
"fee_rate": 1,
"inputs": [{"tx_id": "...", "output_index": 1, "value": 69900000}],
"outputs": [{"to": "...", "amount": 30000000}],
"total_in": 69900000,
"total_out": 30000000,
"change": 39800000,
"built_at_height": 577429,
}
simulate_htlc_lock(*, from_, receiver, hash_lock, timeout, amount, fee=None, fee_rate=None, max_fee=None) -> SimulateHtlcLockResult
Dry-run version of htlc_lock. Same parameters; no broadcast, no
reservation.
{
"size": 231,
"fee": 100000,
"fee_rate": 1,
"htlc_output_index": 0,
"hash_lock": "...",
"timeout": 577500,
"receiver": "...",
"amount": 30000000,
"change": 39800000,
"total_in": 69900000,
"built_at_height": 577429,
}
Payment URI codec
Pure, no I/O. BIP21-style exfer:<address>?amount=...&memo=... round trip.
payment_uri_encode(*, address, amount=None, memo=None, hash_lock=None, timeout=None, label=None) -> str
Build an exfer: URI. Unset fields are omitted. Returns the URI string.
payment_uri_decode(uri: str) -> PaymentUri
Parse an exfer: URI back into its components. address is always
present; the rest appear only if set in the URI.
{
"address": "...",
"amount": 30000000, # present only if set
"memo": "...",
"hash_lock": "...",
"timeout": 577500,
"label": "...",
}
Message signing
sign_message(address: str, message: str) -> SignMessageResult
Sign an arbitrary UTF-8 message with address's key, domain-separated
under EXFER-MSG. Proof of key control for off-chain
challenge-response / agent identity.
{
"signature": "...", # hex128
"pubkey": "...", # hex64
"address": "...", # hex64
}
verify_message(pubkey: str, signature: str, message: str, *, address=None) -> VerifyMessageResult
Verify a message signature (pure crypto). If address is given, valid
is true only when it also matches H(pubkey).
{
"valid": True,
"address": "...", # address derived from pubkey — returned even when valid is False
}
HTLC observability
Walletd's own owned-key index (v1.9). For arbitrary (non-owned)
addresses, run walletd with --indexer-rpc so these route through the
multi-tenant index.
htlc_status(lock_tx_id: str, output_index: int = 0) -> HtlcRecord
Current HtlcRecord for a lock outpoint. Raises WalletdError if
walletd tracks no HTLC there.
{
"lock_tx_id": "...",
"output_index": 0,
"params": {"sender": "...", "receiver": "...",
"hash_lock": "...", "timeout_height": 577500},
"amount": 30000000,
"lock_block_height": 577429, # None while still in the mempool
"state": "locked", # locked | locked_expired | claimed | reclaimed | unknown
"claim": None, # HtlcClaimRecord once claimed
"reclaim": None, # HtlcReclaimRecord once reclaimed
"role": "sender", # sender | receiver | both | observer
"last_indexed_height": 577429,
}
htlc_list(*, role=None, state=None, since_height=None, address=None, limit=None, cursor=None) -> HtlcListResult
List HTLCs matching the filter, paginated. state accepts a single
HtlcState or a list of them; role filters on observer relationship.
{
"htlcs": [ { ...HtlcRecord... } ],
"next_cursor": "...", # present only when more pages exist
}
htlc_forget(lock_tx_id: str, output_index: int = 0) -> bool
Remove a settled HTLC entry from walletd's index. Only Claimed /
Reclaimed entries may be forgotten; pending ones raise WalletdError.
Returns True if an entry was removed, False if nothing was tracked
there.
get_follower_status() -> FollowerStatus
Snapshot of walletd's block-follower state — height, lag, counts.
{
"last_indexed_height": 577429,
"last_indexed_block_id": "...",
"tip_height": 577429,
"lag": 0, # tip_height - last_indexed_height; treat <0 as 0
"indexed_htlc_count": 3,
"follower_started_at": 1700000000,
"full_scan_complete": True,
}
wait_for_tx(tx_id: str, *, min_confirmations=1, timeout_secs=60) -> WaitForTxResult
Block until tx_id has min_confirmations behind it. timeout_secs is
clamped server-side at 600. On timeout raises WaitTimeoutError — not a
terminal failure of the tx, which may still confirm later.
{
"tx_id": "...",
"block_id": "...",
"block_height": 577430,
"confirmations": 1,
}
wait_for_payment(address: str, *, min_amount=1, timeout_secs=60) -> WaitForPaymentResult
Block until a new credit of at least min_amount reaches address.
Returns as soon as the payment is seen in the mempool (0
confirmations) — a receipt/liveness signal, not settlement finality.
timeout_secs is clamped server-side at 600. A quiet window is not an
error: on timeout received is False and timed_out is True, so just
call again.
{
"address": "...",
"received": True,
"timed_out": False,
"tx_id": "...", # None when detected via a confirmed-balance delta
"amount": 30000000,
"confirmations": 0,
"tip_height": 577429,
"waited_secs": 2,
}
Indexer-delegated queries
Multi-tenant queries (v1.9.1) that proxy an exfer-indexer. These
require walletd to be started with --indexer-rpc; otherwise they raise
IndexerNotConfiguredError.
list_settlements(address, *, contract_hash=None, since_height=None, limit=None, cursor=None) -> ListSettlementsResult
History of settled HTLCs involving address.
{
"settlements": [
{
"tx_id": "...",
"block_height": 577430,
"contract_hash": "...",
"outcome": "claimed", # claimed | reclaimed
"observer_address": "...",
"counterparty": "...",
"amount": 30000000,
"lock_tx_id": "...",
"lock_output_index": 0,
},
],
"next_cursor": "...", # present only when more pages exist
}
contract_stats(address, *, contract_hash=None) -> list[ContractStatsRow]
Aggregate stats per contract type for address. Without contract_hash,
one row per distinct contract the address has settled; with it, a single
row (or empty list) for that contract.
[
{
"contract_hash": "...",
"total": 12,
"succeeded": 10,
"refunded": 2,
"avg_settle_blocks": 6,
"last_settled_at_height": 577430,
},
]
get_address_history(address, *, since_height=None, limit=None, cursor=None) -> AddressHistoryResult
Activity timeline for address — every input + output it appears in.
{
"history": [
{
"block_height": 577430,
"tx_id": "...",
"amount": 30000000,
"direction": "output", # input | output
"is_coinbase": False,
},
],
"next_cursor": "...", # present only when more pages exist
}
get_attestation_edges(address, *, contract_hash=None) -> AttestationEdgesResult
Per-counterparty reputation edges for address (contracts run,
succeeded, refunded) — the on-chain trust signal to check before
transacting with a counterparty.
{
"edges": [
{
"counterparty": "...",
"contract_hash": "...",
"contract_name": None, # set only for recognised templates
"total": 5,
"succeeded": 4,
"refunded": 1,
"last_seen_height": 577430,
},
],
}
detect_in_chain_swaps(*, hash_lock=None, limit=None) -> DetectSwapsResult
Groups of HTLCs sharing a hash_lock — the atomic-swap fingerprint.
{
"swaps": [
{"hash_lock": "...", "htlcs": [ { ...HtlcRecord... } ]},
],
}
htlc_lookup_by_hashlock(hash_lock: str) -> list[HtlcRecord]
Every tracked HTLC committed to hash_lock — the canonical atomic-swap
fingerprint. Returns a list of HtlcRecord (see htlc_status).
get_output_spent_by(tx_id: str, output_index: int) -> SpentByResult
Reverse-spend lookup: which tx spent (tx_id, output_index)? source
reveals where the answer came from ("indexer-cache", "node", or
"fallback-unknown-method" for older nodes). spent: false means
nobody has spent it given the indexer's current view.
{
"spent": True,
"spending_tx_id": "...",
"input_index": 0,
"block_height": 577430,
"source": "indexer-cache",
}
get_output_datum(tx_id: str, output_index: int) -> OutputDatum
Read the datum an output carries — the honor verify step. A non-null
quote_id (32 hex) means the 16-byte inline datum was indexed and the
settlement is honor-verifiable. unhonorable is True for a
datum_hash-only output. No datum gives {quote_id: None, unhonorable: False}.
{
"quote_id": "...", # None if the output carries no inline quote_id
"unhonorable": False,
}
find_settlements_by_quote_id(quote_id: str) -> SettlementsResult
Reverse-lookup the on-chain outpoint(s) that settled quote_id (32 hex /
16 bytes) — the honor gate's first step. Zero entries when nothing
references the quote yet; more than one if it was settled multiple times.
Pair each outpoint with get_output_datum to verify the inline quote_id
read-back.
{
"settlements": [
{"tx_id": "...", "output_index": 0},
],
}
Type definitions
Single-value methods return bare Python types (str, int).
Multi-field methods return TypedDicts or NamedTuples — import them
from exfer.types if you want to annotate variables.
from exfer.types import (
AddressHistoryResult,
AddressRecord,
AttestationEdgesResult,
Block,
ContractStatsRow,
DetectSwapsResult,
FollowerStatus,
GenerateAddressResult,
HtlcClaimResult,
HtlcListResult,
HtlcLockResult,
HtlcReclaimResult,
HtlcRecord,
ListSettlementsResult,
OutputDatum,
PaymentUri,
QuoteIssueResult,
QuoteJson,
QuoteVerifyResult,
SettlementsResult,
SignMessageResult,
SimulateHtlcLockResult,
SimulateTransferResult,
SpentByResult,
Transaction,
TransferResult,
Utxo,
UtxosResult,
VerifyMessageResult,
WaitForPaymentResult,
WaitForTxResult,
)
from exfer import Tip # NamedTuple — also top-level
You don't need to import any of these for normal use — they're just
return-type annotations. HtlcState and HtlcRole are Literal
aliases, also in exfer.types.