Skip to content

Releases: UnspendableLabs/Horizon-Market-Client

v0.2.7

Choose a tag to compare

@ouziel-slama ouziel-slama released this 31 Jul 16:34
145cc94

[0.2.7] - 2026-07-31

A faucet button in the wallet's KOR row. A signet wallet holding no KOR is stuck: Kontor charges gas in KOR, an account with none has its operation dropped before execution, and buying KOR would itself cost gas. The pre-flights added in 0.2.5 report that deadlock accurately and leave the user with nowhere to go. This release ships the way out — the Portal's signet faucet, reachable from the wallet screen on web and mobile, next to the balance it fixes.

Added

  • useKontorFaucet() and client.requestKontorFaucet() — ask the signet faucet for KOR. The hook is a small state machine (available, status, result, error, request(), reset()) whose request() never rejects, so a button's onPress needs no try/catch; the client method is the same call for the CLI and for scripts. It credits the connected wallet's taproot key (getAddresses().xOnlyPubkey — the faucet credits a key, not an address) and resolves with the faucet's commit + reveal txids. The KOR lands when the reveal confirms, about a block later, so neither one refreshes balances — the host does that when it dismisses its success UI.
    • available is the render condition, not a disabled state: it is false off Kontor signet (mainnet has no faucet, and a greyed-out button there would advertise free coins that don't exist) and false for a wallet exposing no taproot key. The client method refuses the same cases before making a request.
    • Endpoint defaults to ${baseUrl}/api/kontor-faucet — horizon.market's proxy in front of the Portal faucet, which is where a browser has to go anyway (the Portal sets no CORS headers of its own). Override with the new kontorFaucetUrl client option / provider prop to hit {portal}/api/faucet directly from Node or React Native.
    • No @kontor/sdk behind any of it. It is a plain fetch, so it works in a web bundle built to keep the Kontor WebAssembly out and on a React Native build with no @kontor/sdk-native linked — the very builds where every other Kontor call fails, and where funding an account is what you'd want to do about it. The main entry exports the bare wire call requestKontorFaucet (for a caller holding a recipient key and nothing else) alongside KONTOR_FAUCET_AMOUNT_KOR; /react exports the hook, KONTOR_FAUCET_AMOUNT_KOR and the KontorFaucetResult type.
  • renderTokenAction on WalletBalances (web + native) — where that button goes. The headline token rows (XCP / KOR / ZELD) ship a fixed set of controls — Deposit / Withdraw / Sell — so a host with one more to add had to choose between forking the component and putting its button somewhere else on the page, away from the balance it acts on. The prop is called once per headline token with that row's line and its return value is rendered at the start of the action group, ahead of the SDK's own buttons; return null for the rows you don't extend. Both the horizon.market wallet page and the example mobile app now pass line.symbol === "KOR" ? <KorFaucetAction /> : null and nothing else.
    • The row shape is exported as WalletTokenLine (symbol, amount, error, asset, sellAsset) so the callback can be typed and can branch on the same balance the row prints — including telling "the wallet holds none" from "the read failed", which amount: null plus error already keep apart.
    • Same prop, same position, same type name on both entries, so a host that renders both platforms writes the branch once.
  • The example mobile app ships the faucet (apps/native): a droplet button in the KOR row opening a confirm → requesting → txids modal, with the signet-explorer links and the same two Usermaven events the web wallet page reports (wallet_faucet_opened / wallet_faucet_completed), so the funnel is one chart across web and mobile.

v0.2.6

Choose a tag to compare

@ouziel-slama ouziel-slama released this 31 Jul 14:55
3ffa534

[0.2.6] - 2026-07-31

Added

  • useSellOrderForm() — the packaged sell form's controller, now public. An app rendering its own sell UI had useSellOrder + useAssets and nothing else, so it re-derived by hand every rule sitting between them: the picker's grouping/order/labels, whether the selected asset shows a quantity field, whether the form is submittable, what "Max" means, what to say when one balance source fails rather than comes back empty, and what the result step reports for a listing that broadcast one, two or no transactions. Each of those has a way of being subtly wrong — and one of them is wrong when re-derived naively: Max for KOR is not the balance. Escrowing KOR and paying the attach's gas draw on the same balance and the gas hold lands first, so "list everything" is always a listing whose own gas makes it unexecutable. The controller has priced that in since 0.2.5; hosts couldn't reach it. It also carries the selection reconciliation a hand-rolled form tends to miss: after a balance refresh the selected option is re-pointed at the fresh object (so Max and the balance cap use current numbers) or cleared when the wallet no longer holds it.
    • Exported from /react as useSellOrderForm with AssetGroup, SellTrackTx, SellResultView and UseSellOrderFormResult. It wraps useSellOrder and useAssets and re-exposes both, so a host uses it instead of them, not alongside — one selection, one refresh cycle.
    • Same hook the packaged SellOrderForm renders on both platforms (it moved from react/internal/ to react/hooks/ and was renamed from useSellOrderFormController; it was never exported, so nothing breaks). What a host builds and what the SDK ships now diverge only in markup.
  • Kontor gas pricing re-exported from /react: korCostForGas, maxListableKor, detachGasLimitFromBlob and the three gas limits. They were reachable only from the main entry, which statically imports @kontor/sdk — so a React host wanting to print "gas ≈ 0.0001 KOR" next to a balance had to choose between duplicating the arithmetic and pulling the Kontor WASM backend into a bundle built to avoid it. kontor/gas.ts has no @kontor/sdk import precisely so it can sit on this side of that line.

Fixed

  • The React entries' freedom from @kontor/sdk is now a test, not a manual check. 0.2.5 fixed the web bundle eagerly compiling the Kontor WebAssembly and verified it by inspection — but the property is one accidental re-export away from breaking (a helper pulled from a module that happens to neighbour Kontor code is enough), and nothing about the failure is visible in normal use: the entry just gets heavier, and Hermes builds without @kontor/sdk-native linked start crashing on import. react/bundle-purity.test.ts walks the static import graph of both entries from source and fails on any @kontor/* specifier that survives compilation — following relative imports (erroring on one it can't resolve, so the test can't pass by not looking), and deliberately ignoring import type (erased) and import() (the mechanism that keeps Kontor out of the entry). A companion assertion pins the other direction, because "reaches no Kontor backend" is satisfied just as well by a graph that reaches nothing: the entries must still statically reach kontor/gas.ts, and that file must still be free of @kontor/sdk. Moving the Max affordance's arithmetic behind a module that does pull the backend would keep the first assertion green while the React layer silently lost its ability to price gas at all — so the two are checked together, and with the same import walker, so both directions agree that a bare import "@kontor/sdk" counts and an erased import type does not.

v0.2.5

Choose a tag to compare

@ouziel-slama ouziel-slama released this 31 Jul 14:01
89d263c

[0.2.5] - 2026-07-31

Added

  • client.preflightKontorListing() / preflightKontorPurchase() / preflightKontorDelist() — the Kontor gate below, available as an ordinary read so a review screen can refuse before asking the user to confirm rather than after. They resolve with { ok, error, balanceKor, requiredKor, gasLimit, signerId } instead of throwing, and are read-only: view calls against a read-only session, so no funding UTXOs and no wallet signing prompt — bound to the same identity that would sign (both derive from getAddresses().xOnlyPubkey), because a pre-flight that checked a different account than the one that pays would prove nothing.
    • ok: false carries one of the three refusals — something the wallet can fix. A failure to check (unreachable indexer, Kontor not configured) throws instead: it is not a verdict, and reporting it as one would tell a funded user to go fund their account. balanceKor / requiredKor are filled in either way, so a review screen can print the gas cost next to the balance it just read.
    • The workflows call these very functions — openSellOrder / fillSwaps / delistSwap throw verdict.error — so what a review screen reported and what the broadcast enforces cannot drift apart. There is one check, with two ergonomics.
    • isKontorPreflightRefusal(err) is exported for the throwing side too: a host catching from a workflow can tell a safe, nothing-happened refusal from a real failure without importing three classes to instanceof.
    • horizon sell / horizon buy now run it before their confirmation prompt, and print the gas cost alongside the fee review.
  • React: every packaged review screen refuses a doomed Kontor flow before the user commits (web + native, one implementation each). The workflows already refuse to broadcast, so this changes when the user finds out: previously they confirmed, watched the progress modal, and read the refusal on the result screen. Now the button is disabled with the reason beside it, on all three flows and both platforms:
    • useBuyReview / <BuyReview/> — a Kontor buy has no composable quote to fail on, so the chain read is its equivalent early warning. canConfirm now follows it (it was hard-coded true for Kontor).
    • useSellReview / <SellReview/> — asked off the very params about to be submitted, and the one that saves the most: openSellOrder reserves the listing fee (and can burn a listing credit) before its own gate would find out. Folded into canSign.
    • useSwapConfirmation / <SwapConfirmation/> — gains an optional swap option, used to gate a Kontor delist. The most consequential of the three: a revoke spends the escrow UTXO, so a dropped detach can never be retried.
    • All three expose the verdict as preflight ({ loading, blockedReason, checkError, requiredKor, balanceKor, canSubmit, recheck }), and useKontorPreflight / kontorPreflightNotice are exported from /react so an app rendering its own review UI can gate the same way, with the same wording.
    • A failed check never blocks. A refusal disables the button; an unreachable indexer shows a note and leaves it enabled — blocking there would strand a funded user behind a transient outage, and buy nothing, since the workflow re-runs the same check before it broadcasts anything.

Fixed

  • No Kontor transaction is broadcast any more until chain state says it will actually execute. Every Kontor contract call is gas-metered: the node holds gas_limit × gas_to_token_multiplier KOR from the op's payer before dispatching the call, and rejects the op when the payer can't cover it. That rejection happens ahead of execution, so the node writes no result row — the op leaves no trace in /results, GET /transactions/{txid}/inspect still reports it as Materialized with a null result, and the SDK's poller (which synthesises its events from result rows) never sees it. The Bitcoin side settles regardless: the seller's attach reveal still creates its escrow output and still pays the listing fee; the buyer's swap reveal still pays the seller; a revoke still spends the escrow. On signet this produced a live listing whose NFT was never escrowed, and then a purchase where the buyer paid 3333 sats and received nothing — with every API surface reporting success. All three Kontor flows now gate on chain state first, with view calls only (no signature, no transaction, nothing spent), and every app on this SDK — web, native and the CLI — inherits the gate through openSellOrder / fillSwaps / delistSwap:
    • openSellOrder (listingType: "kontor") checks that the seller can pay the attach's gas and actually holds the NFT / KOR being listed, before the fee quote is reserved (so a blocked listing never burns a listing credit) and before the attach reveal is broadcast.
    • fillSwaps on a Kontor swap checks that the buyer can pay the Sponsor's gas and that the listing's escrow really holds the advertised assetIncomingOffer.inspect() is a structural check on the offer blob and says nothing about chain state, so an unbacked listing was previously indistinguishable from a good one right up until the buyer had paid for it.
    • delistSwap on a Kontor swap checks that the seller can pay the detach's gas, priced from the limit baked into that offer blob rather than assumed. This is the flow whose failure is unrecoverable: a revoke spends the escrow UTXO, so a dropped detach leaves the asset credited to an outpoint that no longer exists and revoke() can never be retried. It was previously ungated.
    • All three emit a new preflightKontor progress step (Kontor openSellOrder / fillSwaps / delistSwap are now 5 steps), and throw KontorInsufficientGasError, KontorAssetUnavailableError or KontorEscrowNotFundedError — all exported, all carrying the amounts involved (requiredKor / availableKor) so a host can render "you need KOR" with a top-up affordance. Every one of them is raised before signing, so the user can fund the account and retry with nothing lost.
    • The gate reads the payer's holder — the signer-id resolved from the session's internal x-only key — and nothing else. Balance display also queries the bech32m-tweaked taproot output key, but that is a different Kontor identity with its own signer-id: the node resolves the payer from the commit envelope's internal key and attach moves the asset from ctx.signer(), so counting the tweaked key's holdings would have let a wallet clear the gate on KOR the node cannot spend — precisely the silent drop being fixed.
    • An unregistered wallet is answered as "holds nothing" without a chain read at all. Every credit path canonicalises its holder-ref to a signer-id row and creates the signer entry on the way, so "no signer row" implies "no holdings" — and a holder-ref view for an unknown x-only key is a deterministic error inside the node, which would have turned the commonest way to hit this gate (a brand-new wallet with no KOR) into a cryptic failure instead of a gas error.
    • Listing KOR now requires the amount plus its own gas. The two come out of one balance and the hold lands first, so "list my whole balance" cleared both checks taken separately and still had its attach dropped. The React sell form's Max button for KOR stops short of the gas for the same reason, and disappears when the balance can't cover even that. (It is arithmetic on the displayed balance, which sums every holder the wallet might use, so it is a good default rather than a verdict — the review step's preflight is what answers authoritatively.)
    • Pricing the gas and checking the asset are one pair of reads, not two. The gas half already resolves the signer-id and the KOR balance, so a listing pre-flight hands both to the asset half instead of asking the indexer the same two questions again — halving its round-trips and removing a window where the two halves of one verdict could describe different moments.
    • An indexer failure during the check surfaces as an error, never as "you have no KOR": the signer lookup distinguishes a 404 (unregistered ⇒ genuinely zero) from a failed request. The workflows pass the client's own fetch into it (KontorContext gained the field), so a host-supplied fetch — a proxy, a React Native polyfill — is honoured here as it already is by every other indexer read; bypassing it would make the check fail to reach an indexer the rest of the SDK reaches fine, and a failed lookup throws, so it would block the very flow it protects.
    • A listing that doesn't record which asset its escrow holds is treated as unverifiable, not unbacked. kontorAmount / kontorNftId / kontorContractAddress are each nullable on AtomicSwap — a pre-convention or third-party listing may name its escrow but not its contents — so the escrow half is skipped (as it already was for a missing assetUtxoId) and the gas half still runs. Treating it as a parameter error instead would have taken a listing that is buyable today and made it unbuyable behind a cryptic throw. A field that is present but malformed still throws: corrupted listing data is worth surfacing rather than silently disarming the check.
    • onProgress can no longer break a workflow. A throw from the host's progress callback used to propagate out of the step that had just succeeded — on acceptKontorOffer that meant an already-broadcast swap reveal surfacing as a raw callback error instead of KontorPurchaseNotRecordedError, losing the txid the recovery needs; on the new pre-flight step it would have let the Kontor session escape its try/finally unclosed. Progress reporting is telemetry, so it is now contained: a throwing listener is swallowed, the step's own error is never replaced by it, and later events still fire.
    • Also exported: korCostForGas(gasLimit), detachGasLimitFromBlob(blob), `ma...
Read more

v0.2.4

Choose a tag to compare

@ouziel-slama ouziel-slama released this 30 Jul 16:53
cd9d292

[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 v1v2, 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.

v0.2.3

Choose a tag to compare

@ouziel-slama ouziel-slama released this 23 Jul 10:36
d1a0832

[0.2.3] - 2026-07-23

Added

  • React: deposit / withdraw observer callbacks on <WalletBalances/> (web + native) — onDeposit, onWithdraw, and onWithdrawComplete. Previously the only host-observable action was onSellAsset; deposit and withdraw happened entirely inside the component, so a host could not track them. These three are observers, not overrides: the built-in deposit modal and withdraw form still drive the UX unchanged — the callbacks only notify.
    • onDeposit(event: WalletDepositEvent) fires when a deposit address is opened ({ symbol, depositType, asset? }asset set when opened from a specific holding).
    • onWithdraw(event: WalletWithdrawEvent) fires when a withdraw flow is opened for a balance ({ target }, covering BTC + every asset kind).
    • onWithdrawComplete(event: WalletWithdrawCompleteEvent) fires when a withdraw broadcasts successfully ({ target, txid }).
    • New exported types: WalletDepositEvent, WalletWithdrawEvent, WalletWithdrawCompleteEvent. The shared useWalletBalancesController now accepts an optional { onDeposit, onWithdraw } and exposes openWithdraw(target), so both renderers fire the callbacks identically.

v0.2.2

Choose a tag to compare

@ouziel-slama ouziel-slama released this 22 Jul 15:21
2748a45

[0.2.2] - 2026-07-22

Added

  • useSwapList / SwapList: price-range and collection filters. New options defaultPriceMin / defaultPriceMax (inclusive sat bounds) and defaultCollection (a collection slug), surfaced as reactive state priceMin / priceMax / collection with setters setPriceRange(min, max) and setCollection(slug). Both reset to the first page and re-query listSwaps with the matching priceMin / priceMax / collection params (added to the client in 0.1.2); unset bounds are omitted from the query (sent as undefined, never null). Pair setPriceRange with a price bucket's sat-bounds from the facets below.
  • useSwapList / SwapList: opt-in reactive facet counts via includeFacets. When on, one getSwapFacets request (added in 0.1.2) runs per filter change — never one per option — using the exact same filters as the feed (minus sort/pagination), and the counts are exposed as facets (per type, per price bucket, per collection) and facetsLoading. Stale responses are seq-guarded, the last-known counts stay visible while the next request is in flight (no flicker to empty), and a facets failure is swallowed — it never surfaces the main error banner. Powers a faceted filter sidebar with live, filter-aware counts.
  • New @unspendablelabs/horizon-market-client/swaps subpath — a WASM-free entry that re-exports the SDK's pure swap-list logic: the display derivations (swapDisplayName, swapDisplayTitle, swapListItemView, swapMonogram, swapImageUrl, swapDisplayQuantity, swapDisplayPricePerUnit, pendingSwapTrackingTxid, formatQuantity), the list utilities (mergeSwapsById, sortSwaps, paginateSwaps, clampPage, getSellerAddresses, checkIsMySwap), and the list constants / sort presets / types (DEFAULT_LIMIT, FILTER_TABS, SORT_OPTIONS, SORT_OPTION_LABELS, SORT_MAP, SortOption, SwapListingType, SwapListOrderBy, SwapListOrder, the PENDING_ORDERS_* / MY_SWAPS_* caps). Unlike the main (.) and ./react barrels — which statically import @kontor/sdk and eagerly compile WebAssembly on import — this entry has zero runtime dependency on @kontor/sdk, bitcoinjs-lib, @scure/*, ecpair, or React, so it imports cleanly in Node unit tests and SSR-reachable code. It lets a host build its own faceted swap browser on the exact pure logic the packaged <SwapList/> uses, without duplicating it.

Changed

  • (internal, no consumer-facing change) The swap-list sort presets, list constants, and the SortOption / SwapListingType / SwapListOrderBy / SwapListOrder types moved out of the React useSwapList module into the WASM-free swapListConstants module (the one now published at ./swaps), removing the circular import between them. They stay re-exported from the ./react entry, so existing imports (SORT_OPTIONS, SORT_OPTION_LABELS, SortOption, …) are unchanged.

v0.2.1

Choose a tag to compare

@ouziel-slama ouziel-slama released this 21 Jul 16:12
1053171

[0.2.1] - 2026-07-21

Added

  • React: new onSellAsset?: (asset: AssetOption) => void prop on the web <WalletBalances/> — override the per-asset "Sell" action. When provided, clicking Sell (on the XCP / KOR / ZELD headline tokens and on every "other holdings" tile) calls the host callback with the pre-selected asset instead of opening the built-in inline <SellOrderForm/> modal — e.g. to navigate to a dedicated Sell screen. When omitted, the internal modal is used as before. Brings the web component to parity with the native WalletBalances, which already exposes onSellAsset.
  • Counterparty subasset long names are now captured and surfaced for display. Subassets list on-chain under a numeric A… name; the human-readable long name (e.g. PEPENARDO.CARD) was previously fetched from the API but discarded. Now:
    • CounterpartyBalance gains assetLongname: string | null (read from the balances endpoint's asset_info.asset_longname).
    • AtomicSwap gains assetLongname?: string | null (mapped from the asset_longname field the Horizon backend resolves on listSwaps / getSwap).
    • The owned-asset AssetOption (counterparty variant) gains assetLongname?: string | null.
  • React: every packaged display surface now shows assetLongname ?? assetName, so subassets read as their long name instead of A4950… — the swap-list tiles / <SwapList/>, the buy & sell review screens (<SwapConfirmation/> / <SellOrderForm/>), and <WalletBalances/> (headline + "other holdings" tiles, deposit / withdraw modals, and the placeholder monogram). assetName remains the identifier used for image resolution and API calls; only the display label changed.

Changed

  • Withdraw / send: the wallet signature is now requested at broadcast time, on confirm — not while preparing the review screen. prepareSend (and prepareBtc / prepareCounterparty / prepareZeld / prepareOrdinal) now compose and fund the transaction but leave it unsigned; PreparedSend.broadcast() signs it (prompting the wallet) then publishes. The packaged <WalletBalances/> withdraw flow therefore shows its confirmation screen first and only pops the wallet when the user hits "Confirm & send". feeSats is unchanged — it is computed from UTXO selection at compose time, so the review screen still shows the exact miner fee before any signature. Kontor sends (kor / kontor-nft) were already sign-on-broadcast. SendClient.send() / sendAsset() (the one-shot prepare().broadcast() helpers) are unaffected.

v0.2.0

Choose a tag to compare

@ouziel-slama ouziel-slama released this 21 Jul 09:47
9f73c12

[0.2.0] - 2026-07-20

Added

  • Asynchronous signers: Signer.signPsbtHex / signMessage may now return string or Promise<string>, so a custom Signer can delegate to an external wallet (browser extension / mobile) that signs through a prompt and never exposes its key. Every workflow (openSellOrder, fillSwaps, delistSwap), send helper, and signAndFinalizeSellPrep awaits the result; LocalSigner / HDSigner are unchanged (still synchronous). getAddresses() stays synchronous.
  • Kontor now works with external wallets (browser extension / mobile — Xverse, Horizon Wallet, …), not just in-process key signers. When a Signer doesn't implement getKontorSigning but exposes a Taproot address and its x-only public key (getAddresses(){ p2tr, xOnlyPubkey }), the SDK builds a wallet-backed Kontor Signing that delegates transaction signing to signPsbtHex and message signing to signMessage — so openSellOrder / fillSwaps / delistSwap for KOR & Kontor-NFT listings run through the connected wallet's signing prompt, key never exposed. The wallet's internal taproot x-only key is Kontor's identity (re-tweaked to the P2TR address, which is asserted to match the wallet's own so a wrong key/network fails loudly). BLS registration (raw Schnorr-over-digest) is the only Kontor capability unavailable this way, and no marketplace flow needs it. Adds @scure/btc-signer / @scure/base as direct dependencies.
  • React: HorizonMarketProvider context gains initializeWithSigner(signer) — connect from a host-supplied Signer (external wallet) instead of a raw key or phrase; addresses come from the signer, exportMnemonic() returns null, and sessionSource reports "external".
  • React: new autoSignIn prop on HorizonMarketProvider (default true) — set false to skip the automatic BIP322 wallet sign-in on connect (for hosts that authenticate another way, e.g. a same-origin session cookie, or an external signer whose message signing would pop the wallet). Sign-in stays available on demand via client.signInWithWallet().
  • React: exported the sell-review data layer — useSellReview plus SellCost, UseSellReviewArgs, UseSellReviewResult, FeeOption, and the FEE_HINTS / FEE_LABELS / FEE_OPTIONS constants. It powers the packaged <SellOrderForm/> confirm step (listing/attach/network cost breakdown, live fee-rate selection, fee waiver, Kontor listing + attach-miner fee estimates); exporting it lets a host render its own confirmation UI on the exact same data, the way apps already build on useSellOrder.
  • React: exported the buy-review data layer — useBuyReview plus UseBuyReviewArgs / UseBuyReviewResult (its FeeOption and the FEE_HINTS / FEE_LABELS / FEE_OPTIONS constants are already exported via useSellReview). It powers the packaged <SwapConfirmation/> buy confirm step (price + royalty + buyer miner-fee breakdown, live fee-rate selection, Kontor estimates); exporting it lets a host render its own buy confirmation UI on the exact same data, symmetric with useSellReview.

Changed

  • BREAKING: signAndFinalizeSellPrep(quote, signer, btcNetwork) is now async and returns Promise<SignedSellPrepResult | undefined> (was synchronous), so it can await asynchronous signers. Add await at call sites.

Fixed

  • Native example app (iOS): fixed a launch crash on TestFlight and physical iPhones — dyld: Library not loaded: @rpath/RNWorklets.framework/RNWorklets. Expo 57's precompiled native modules shipped a dynamic RNReanimated.framework that links @rpath/RNWorklets.framework, but that RNWorklets.framework was not embedded in the device IPA, so dyld failed before JS started (the simulator build embedded it and hid the issue). react-native-reanimated and react-native-worklets are now forced to build from source via expo.autolinking.ios.buildFromSource; under the app's static linking they compile into static libs, so no dynamic RNWorklets.framework is produced and the @rpath load disappears. Both must be source-built together — building only worklets leaves the precompiled reanimated referencing the un-embedded framework.

v0.1.2

Choose a tag to compare

@ouziel-slama ouziel-slama released this 17 Jul 15:23
e77602d

[0.1.2] - 2026-07-17

Added

  • Price-range and collection filters on listSwaps: new ListSwapsParams.priceMin / priceMax (inclusive bounds in sats) and collection (a collection slug the server expands to its asset names, so only Counterparty listings match) — combinable with every existing filter
  • HorizonMarketClient.getSwapFacets(params?, options?) — one request returns reactive facet counts for a filter set (SwapFacets: listings per type, per price bucket, and per collection); each dimension is counted excluding its own active selection so sibling options keep clickable, non-zero counts. Takes the same filter shape as listSwaps minus pagination/sort (SwapFacetsParams); new types SwapFacets, SwapFacetsParams, PriceBucketFacet (USD presets resolved to sat-bounds server-side), and CollectionFacet

v0.1.1

Choose a tag to compare

@ouziel-slama ouziel-slama released this 17 Jul 10:22
c53a0f6

[0.1.1] - 2026-07-17

Added

  • Pending-order surfacing on listSwaps: new ListSwapsParams.pendingAddress (a single address, or an array — one query prioritizes a wallet's in-progress orders across all its addresses) plus two new AtomicSwap fields, pendingRole ("seller" | "buyer" | null) and pendingTxid (string | null), populated only for that address's pending rows and null everywhere else
  • useSwapList / SwapList — opt-in includePendingOrders surfaces the connected wallet's in-progress orders (pending sell listings still settling on-chain + pending purchases whose buy tx is unconfirmed) as pendingOrders, self-polling until each tx confirms; trackPendingBuy(swap, txid) optimistically shows a just-made Kontor buy immediately, independent of the server's pending_address decoration
  • "Sold" feed on useSwapList / SwapListdefaultShowSold option and showSold / setShowSold / canShowSold: browse completed sales (the whole marketplace's, or narrowed to the wallet's own when combined with "My swaps"), as an independent dimension from the "My swaps" filter
  • HorizonMarketClient.recordKontorPurchase(swapId, { buyerAddress, txId }) — safe recovery that replays only the recording POST for a Kontor purchase whose swap reveal is already broadcast on-chain, never re-accepting (and re-broadcasting) the consumed offer
  • kontorPurchaseRecovery(err) helper and the KontorPurchaseRecovery type — extract { swapId, txId, buyerAddress } from a caught KontorPurchaseNotRecordedError without pulling the heavy @kontor/sdk backend into the main bundle

Fixed

  • Kontor KOR / NFT balance reads now resolve the wallet's registered Kontor signer-id (cached per x-only pubkey) and sum balances across every plausible holder ref (signer-id + both x-only-pubkey forms) — a wallet whose assets were credited to its signer-id previously read as a zero balance
  • Kontor purchase recording failures now surface the real underlying cause instead of a generic error, and support a safe record-only retry