You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
[0.2.4] - 2026-07-30
Added
React: buy / delist funnel observer callbacks on useSwapList / <SwapList/> (web + native) — onLoginRequired, onBuyStarted, onDelistStarted on useSwapList, and onBuySuccess, onBuyError, onDelistSuccess, onDelistError on <SwapList/>. Previously a host could only observe the raw onSwapSelect tap (fires for both buy and sell) with no visibility into the login/confirm/result funnel — buy/delist results were consumed entirely inside the component. These are observers, not overrides: the built-in login modal, confirmation modal, and result screens still drive the UX unchanged — the callbacks only notify, each passed the full AtomicSwap (not just the asset-poor PendingSale) so a host can build rich analytics and branch kontor-vs-multisig via swap.listingType.
onLoginRequired(swap) fires from onItemAction when acting while logged out, before the login modal opens.
onBuyStarted(swap) / onDelistStarted(swap) fire when the buy/delist confirmation modal opens — immediately, or after a successful login.
onBuySuccess(swap, sales) / onBuyError(swap, error) and onDelistSuccess(swap) / onDelistError(swap, error) fire from SwapList's existing confirmation wiring.
Fixed
A failed balance read no longer masquerades as an empty wallet.WalletBalances (web + native) rendered "No {group} holdings yet." whenever a tab had no options — including when that source's fetch had failed, since useAssets resolves a rejected source to an empty list and only records the reason in errors, which the component never read. An unreachable indexer, a down API, or a wallet the SDK can't identify all produced a confident, wrong "you own nothing" with no way to tell. A failure is only one of three ways an empty group can lie, so useAssets now publishes the whole picture as sources: Record<SourceKey, SourceState> — loading (the read hasn't settled; previously every tab claimed "no holdings" for the length of the first fetch), ok, error, or unread (this app never asks for that source: no ordApiBaseUrl, Kontor off this network). Each OtherGroup carries its source's state, and the shared emptyGroupNotice() decides what both renderers show — only ok licenses the "No {group} holdings yet." claim and its deposit affordance. A failure also marks the tab (!) so it's visible from a non-active tab; unread and loading don't, being nothing to alarm the user about. unread deliberately stays out of errors: nothing went wrong and there is nothing to retry.
isEmpty no longer counts a failed read as an empty wallet. It was loadedOnce && !allAssets.length — true even when every source had failed, which is exactly the conflation the rest of this fix removes. It now also requires that no source failed, so the empty states driven by it are only ever shown for a wallet that was actually read. SellOrderForm's asset picker gets a distinct "Couldn't load your assets" placeholder for that case (the per-source reasons were already listed under it).
The headline balances stop reporting a failed read as 0. The same lie sat above the tabs: XCP / KOR / ZELD fell back to "0" whenever their source yielded no holdings, failure included. TokenLine now carries an error (XCP ← counterparty, ZELD ← zeld, KOR ← kontor, BTC ← the BTC read) and a null amount when set; all four render sites (web + native × full list + dropdown summary) print — instead of a balance they could not read, with the reason in the tooltip / accessibility label — and spelled out under the BTC line, which has room for it. This also gives errors.zeld a consumer: ZELD has no other-holdings tab, so its failure was previously unreportable.
getKontorHoldings() now reports why it read nothing. It deliberately degrades to empty holdings rather than throwing (a Kontor outage must not break the whole balances read), so its silent bail-outs — no @kontor/sdk backend, a non-signet network, or a wallet exposing no taproot x-only key — were indistinguishable from an empty wallet and never reached errors.kontor at all. The result gains unavailable: KontorUnavailableReason | null ("runtime" | "network" | "wallet-key"), which useAssets turns into a user-facing errors.kontor. Null both on success and when Kontor is simply not configured. Note: consumers that construct a KontorHoldings (test doubles) must add the field.
CLI: horizon balances stops printing 0 for a source it couldn't read. Same bug, same shape — every read was folded into an empty list on rejection. XCP / ZELD / KOR now print — when their read failed (KOR also when getKontorHoldings() reports unavailable), each failure is named under the table it explains, and --json gains an errors object (null per source on success) so scripts can tell "holds none" from "couldn't look". The BTC price feed is tracked as its own source (errors.price) — without it a down feed just dropped the (…) USD suffix, reading as "no price for you" — and an ord API that didn't answer is reported under its own Ordinals heading rather than printing nothing at all, which is what an inscription-free wallet also does.
The balances cache no longer persists a failed read — or re-fetches everything because of one. It stored holdings only, so a later mount seeding from it reported no errors and replayed the silent, wrong "you hold nothing" for the rest of the one-hour TTL. The stored payload is now { assets, stale } (cache key bumped v1 → v2, old entries read as a clean miss): the sources that answered are cached, the ones that failed are named, and the next mount paints the cached part immediately while re-fetching only the stale sources. Dropping the whole snapshot instead would have made one flaky source — the ord API, typically — cost a full re-read of the other three on every mount until it recovered. A topped-up entry keeps its original timestamp, so a source that keeps failing can't extend the others' TTL indefinitely.