Commit Graph
2105 Commits
Author SHA1 Message Date
Claude 311c9d13d9 feat(cashu): account-load filter + LocalCache dispatch for NIP-60/61 kinds
Two fixes that together make sure NIP-60 / NIP-61 events actually reach
the cache and the always-on account loader picks them up.

1) LocalCache dispatch
   Before: justConsumeInnerInner's `when (event)` block had no branches
   for any of CashuWalletEvent / CashuTokenEvent /
   CashuSpendingHistoryEvent / CashuMintQuoteEvent / NutzapEvent /
   NutzapInfoEvent. Events of those kinds were dropped on the floor by
   the cache — only our private re-broadcast through
   account.sendLiterallyEverywhere() (which calls
   cache.justConsumeMyOwnEvent directly, bypassing the dispatcher) made
   the wallet visible. Events arriving cleanly from relays were
   silently lost.

   Now: each kind dispatches through the same consumeBaseReplaceable
   (kinds 17375, 10019) / consumeRegularEvent (7374, 7375, 7376, 9321)
   paths used by every other Nostr kind in the app. Token events,
   history, mint quotes, and inbound nutzaps now land in LocalCache
   from any source (relay, restore-from-prefs, manual paste, …).

   To make CashuWalletEvent dispatchable through consumeBaseReplaceable
   (which requires AddressableEvent), promote it from `Event` to
   `BaseReplaceableEvent`. The static helper `createAddress(pubKey)`
   stays for callers that don't have an instance; FIXED_D_TAG kept for
   backwards source compatibility.

2) Account-load filter
   AccountInfoAndListsFromKeyKinds2 (the always-on per-account
   subscription that loads kind:0 / NIP-65 / mute list / etc. on
   signin) now also pulls kind:17375 and kind:10019. This means even
   users who never open the wallet screen have their wallet event and
   nutzap-info indexed against their home-relay set — so the wallet is
   ready to render the moment they do open it, and inbound nutzaps can
   target a known kind:10019 without a separate fetch.

   Note: this doesn't replace CashuWalletFilterAssembler — that one
   runs against outbox relays and also fetches the non-replaceable
   kinds (7374, 7375, 7376, 9321). Both are needed; the relay client
   dedupes overlapping filters on the wire.

playDebug + fdroidDebug compile clean; 24/24 NIP-60 jvm tests still
pass (BdhkeTest × 7, AmountSplit × 7, P2PK × 6, MintException × 4).

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:41 +00:00
Claude bfd00ccc34 feat(cashu): send NIP-61 nutzaps from the zap picker
Adds an end-to-end "Nutzap" path to the existing zap chooser popup.

Protocol layer (quartz)
  * CashuMintOperations.swapToLocked: mints P2PK-locked outputs for a
    recipient pubkey alongside the unlocked change. Uses the new
    lockedOutputFor() helper which encodes NUT-11 P2PK secret strings
    before blinding.
  * NutzapInfoEvent.createAddress() mirrors CashuWalletEvent's helper so
    LocalCache.getOrCreateAddressableNote can look up a recipient's
    kind:10019 by pubkey alone.

Wallet ops (amethyst)
  * CashuWalletOps.sendNutzap: spends [available] proofs at [mintUrl] to
    produce locked outputs worth [amountSats], publishes a kind:9321 with
    those proofs + the zappedEvent + recipient p-tag, rolls leftover
    change into a new kind:7375 (with `del` referencing the sources),
    NIP-09-deletes the source token events, and logs kind:7376 (direction
    OUT, destroyed/created references).
  * CashuWalletState.peekNutzapTarget(recipient): pure read against the
    cached kind:10019 + our mint set. Returns a NutzapTarget (mint URL +
    recipient P2PK pubkey) if (a) we have a Cashu wallet, (b) recipient
    published kind:10019 with a P2PK pubkey, and (c) we share at least
    one mint with them. Returns null otherwise so the UI can hide the
    nutzap chip.
  * CashuWalletState.sendNutzap: orchestrates target lookup + ops call.

UI integration
  * ReactionsRow.ZapAmountChoicePopup gains a `nutzapEnabled: Boolean`
    parameter. When true, the popup renders a NutzapAmountChip per zap
    amount (tertiary-color, wallet icon) inline with the existing LN +
    on-chain chips. Tap fires AccountViewModel.sendNutzap which
    forwards into CashuWalletState.sendNutzap. Errors surface via the
    same toast path as LN-zap errors.
  * ReusableZapButton computes nutzapEnabled from the recipient's
    cached kind:10019; chip is hidden when no nutzap target resolves.

Sender currently has to have the recipient's kind:10019 already in
LocalCache for the chip to appear (typical when viewing a note whose
author the user has interacted with). Background prefetch of kind:10019
for unfamiliar authors is a follow-up.

24/24 NIP-60 jvm tests still passing. Both playDebug and fdroidDebug
compile clean.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:40 +00:00
Claude bbd43e34e9 fix(cashu): close publish-bridge race + lock in exception types via tests
Replaces the `var publishDelegate` set-after-construction pattern with an
explicit `CashuWalletState.start(publish: suspend (Event) -> Unit)`.
Account now calls `cashuWalletState.start { event -> sendLiterallyEverywhere(event) }`
from its own init { } block, AFTER all field initializers complete.

Why this matters: the previous code launched the backfill + cache-live
collectors from inside the state's own init { } block. Those collectors
could (and would, for returning users) fire an auto-redeem during
Account's field-initializer phase — at which point `publishDelegate` was
still the no-op default AND `followPlusAllMineWithIndex` (which
sendLiterallyEverywhere depends on) wasn't initialized yet. The publish
would silently swallow or NPE. Gating all of start()'s work behind a
@Volatile started flag eliminates the window.

Also: `MintExceptionTest` (+4 tests) pins down the runtime-exception
contract of `MintHttpException` and the new `MintProtocolException` —
the latter is what callers branch on when distinguishing "mint refused"
from "HTTP failed". Kept simple so any future refactor that breaks the
hierarchy fails loudly here instead of silently in describeMintError.

24/24 NIP-60 jvm tests passing.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:40 +00:00
Claude 5cd756cea2 refactor(cashu): lift wallet state to Account, react to live cache updates
Addresses the critical findings from the post-implementation audit:

A1. State holder lives on Account, not the ViewModel
  New CashuWalletState owns the wallet event, decrypted token contents,
  history, mint-quote, and inbound-nutzap indexes. It's constructed on
  Account and runs for the lifetime of the login session — so nutzaps
  arriving while the user is on Home/DMs/etc. get auto-redeemed without
  requiring the wallet screen to be open. ViewModel becomes a thin
  presenter that forwards flows + holds per-flow UI state (mint quote
  in progress, melt confirmation pending).

A2. Reactive observation via LocalCache.live.newEventBundles
  The state object backfills once from cache.notes at construction time,
  then receives incremental updates from the live new/deleted event
  bundles for any NIP-60/61 event authored by us (or addressed to us
  via #p for nutzaps). NIP-44 decryption results for kind:7375 events
  are cached by event-id, so the per-refresh re-decrypt is gone (D2).

A3. Mutex-guarded auto-redeem (no more duplicate /v1/swap races)
  redeemPendingNutzapsSerialized uses tryLock so a sweep already in
  flight short-circuits any new triggers; subsequent cache updates
  catch up via the next bundle.

A4. Mint-quote recovery on launch
  pendingQuotes flow surfaces unfulfilled kind:7374 events whose
  expiration hasn't passed and whose id isn't yet referenced with a
  "destroyed" marker in any kind:7376. ViewModel.resumeMintQuote()
  re-polls the mint for the original quote and rebuilds the flow.

B1. NutzapInfoEvent now carries the wallet's outbox relays so senders
  publish nutzaps where our assembler is actually listening.

B2. Subscription tracks the outboxRelaysFlow — when the relay list
  changes, the assembler subscription is rebuilt with the new set.

B5. New MintProtocolException distinguishes "HTTP fine, protocol said
  no" (e.g. melt state != PAID) from "HTTP error". Both surface
  through describeMintError() (now top-level — C4).

B7. redeemNutzap now pre-checks the P2PK secret's pubkey matches our
  wallet pubkey before signing — saves a wasted mint round-trip when
  the lock targets someone else.

B8. Melt is a two-phase flow: startMelt() returns a Quoted state with
  amount + fee_reserve so the UI confirms before paying; confirmMelt()
  actually spends. No more silent fee acceptance.

C1. MintHttpClient + CashuMintOperations cached per mint URL via a
  ConcurrentHashMap.

C3. AddCashuWalletScreen has a "Verify" button that pings /v1/info
  before adding, with inline success / failure feedback.

C7. Inline JsonObject FQN in P2PK.kt replaced with proper import.

C8. Dead .also { _ -> secretJson } removed from redeemNutzap.

D1. runCatching {}.getOrNull() callsites in the state holder now log
  via Log.w("CashuWallet") so silent failures surface in logcat.

D5. CashuWalletQueryState made @Immutable + data class for Compose
  stability hygiene.

Touched files: Account.kt (state field + constructor params),
AccountCacheState.kt + AppModules.kt (wire the assembler factory +
okHttpClientForMoney through), CashuWalletOps.kt (decouples from
Account, takes signer + publish callback), CashuWalletState.kt (new),
CashuWalletViewModel.kt (presenter rewrite), CashuWalletScreen.kt
(two-phase melt UI), AddCashuWalletScreen.kt (Verify button),
strings.xml (new keys).

All 20 NIP-60 jvm tests still pass; playDebug + fdroidDebug compile
clean.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:40 +00:00
Claude 16401e536a feat(cashu): full NIP-60 wallet + NIP-61 nutzap receive
Builds out the Cashu wallet beyond the scaffold: a complete mint
protocol layer, the four user-facing wallet operations (mint, melt,
send-as-token, redeem), and auto-redemption of inbound NIP-61
nutzaps. Wires the relay subscription so the wallet state syncs
across devices.

quartz/ — mint protocol layer (commonMain + jvmAndroid)
  * nip60Cashu/mintApi/MintApiDtos.kt — Kotlinx Serialization DTOs
    for NUT-00..06 (info, keys, mint/quote/bolt11, mint/bolt11,
    swap, melt/quote/bolt11, melt/bolt11, checkstate). ProofDto
    carries the optional NUT-11 witness.
  * nip60Cashu/mintApi/MintHttpClient.kt — OkHttp + kotlinx-json
    client bound to a single mint URL; surfaces MintHttpException
    with the mint's detail string preserved for the UI.
  * nip60Cashu/mintApi/CashuMintOperations.kt — combines BDHKE +
    HTTP + amount splitting. Exposes requestMintQuote / mintProofs
    / swap / requestMeltQuote / meltProofs / redeemNutzap. Power-
    of-2 amount split per NUT-00.
  * nip60Cashu/mintApi/AmountSplit.kt — extracted into commonMain
    for testability.
  * nip60Cashu/p2pk/P2PK.kt — NUT-11 locked-secret format and
    BIP-340 Schnorr witness signing.
  * CashuProof gains an optional witness field.

amethyst/ — wallet ops + UI
  * model/nip60Cashu/CashuWalletOps.kt — Nostr publishing layer
    over CashuMintOperations:
      - publishWalletEvents (kind 17375 + kind 10019 together)
      - startMintFromLightning / checkMintQuote /
        completeMintFromLightning (kind 7374 lifecycle + 7375 +
        7376 + NIP-09 deletion of the quote)
      - meltToLightning (pre-swap if needed, melt, change rollover,
        delete sources, history)
      - sendAsToken (swap to exact split, V4Encoder for cashuB,
        rollover, history)
      - redeemToken (inbound cashuA/B via swap)
      - redeemNutzap (NIP-61 P2PK unlock + swap, history with
        unencrypted "redeemed" marker per spec)
  * service/cashu/v4/V4Encoder.kt — inverse of the existing
    V4Parser; encodes proofs to cashuB strings for send.
  * ui/screen/loggedIn/wallet/CashuWalletScreen.kt — adds four
    action buttons (Receive / Send LN / Send Token / Redeem) with
    AlertDialog-based flows that poll the mint quote, paste/copy
    from clipboard, and surface mint errors.
  * ui/screen/loggedIn/wallet/CashuWalletViewModel.kt — new mint
    / melt / send-token / redeem state machines, subscribes via
    CashuWalletFilterAssembler on init (auto-syncs the wallet
    across devices), observes the wallet note's flow for reactive
    refresh, and auto-redeems any inbound kind 9321 nutzap that
    isn't already marked redeemed in our kind 7376 history.

relay subscription
  * commons/.../CashuWalletFilterAssembler.kt refactored into the
    standard ComposeSubscriptionManager + SingleSubEoseManager
    pair (matches the NWC pattern). Now driven by subscribe(query)
    / unsubscribe(query) calls from the ViewModel.
  * RelaySubscriptionsCoordinator.cashuWallet exposes a singleton
    assembler reachable as Amethyst.instance.sources.cashuWallet.

Tests (jvmTest)
  * BdhkeTest — 7/7
  * AmountSplitTest — 7/7 (NUT-00 vectors + sum invariants)
  * P2PKTest — 6/6 (secret round-trip, witness verifies under
    BIP-340, compressed + x-only acceptance)

Total: 20 new NIP-60 jvm tests, all passing. Both playDebug and
fdroidDebug compile clean.

Deferred (clearly bounded follow-ups):
  * Sending nutzaps (kind 9321) from the zap picker UI — requires
    integrating with the existing LN zap chooser surface. The
    underlying P2PK locking primitives are in place.
  * Recovering an interrupted kind 7374 mint quote on next launch
    — current flow keeps polling while the dialog stays open.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:40 +00:00
Claude a64cc274cc feat(amethyst): scaffold NIP-60 Cashu wallet UI + state
Adds the user-visible scaffolding for a Cashu wallet alongside the
existing NWC wallets. View-only for now — minting, send/receive, and
NIP-61 nutzaps land in a follow-up commit on this branch.

UI
  * AddWalletScreen is now a wallet-type chooser. The existing NWC
    flow moves verbatim to AddNwcWalletScreen; AddCashuWalletScreen
    is new: takes one or more mint URLs, auto-generates a separate
    P2PK key for nutzap receiving (or accepts a pasted hex key), and
    publishes a kind:17375 wallet event via the account's signer
    using CashuWalletEvent.build(mints, privkey).
  * CashuWalletScreen renders the wallet's mint list, total balance
    in sats (summed across all unspent kind:7375 token events the
    signer can decrypt, with rollover applied via the `del` field),
    and a chronological history view sourced from kind:7376.
  * WalletScreen surfaces the Cashu wallet as a card under "Your
    Wallets" when one exists, so the Wallets entry point shows both
    wallet kinds side by side.

Relay subscription
  * CashuWalletFilterAssembler (commons) subscribes one filter per
    relay covering kinds 17375/7375/7376/7374/10019 by author and
    one targeting inbound kind:9321 via #p. Not yet wired into
    Account.kt — the view path works because we feed our own writes
    through cache.justConsumeMyOwnEvent. Cross-device sync requires
    the assembler subscription wiring, which comes next.

Plumbing
  * Routes.WalletAddNwc / WalletAddCashu / CashuWallet added and
    registered in AppNavigation.
  * CashuWalletEvent.createAddress(pubKey) mirrors MetadataEvent for
    looking up the replaceable wallet event from LocalCache.

Compiles clean on playDebug + fdroidDebug; BDHKE jvm tests still
pass (7/7).

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:39 +00:00
Claude 023e2df542 feat(quartz): add BDHKE primitives for NIP-60 Cashu wallets
Implements blind Diffie-Hellman key exchange per NUT-00 — the
cryptographic core that lets a Cashu mint sign blinded messages
without seeing the underlying secret. Used by the upcoming NIP-60
wallet flows (mint, swap, melt) to issue and verify ecash proofs.

- hash_to_curve (NUT-00 try-and-increment, with Cashu domain separator)
- blind:    B_ = Y + r·G
- unblind:  C  = C_ - r·K
- sign/verify: mint-side helpers used by tests and DLEQ-less
  client-side proof validation.

All operations sit on top of the existing pure-Kotlin secp256k1
implementation in quartz/utils/secp256k1/, so they run on every KMP
target without JNI. Includes the official NUT-00 hash_to_curve test
vectors and a BDHKE round-trip with both the trivial (a=1, r=1) and
a random key.

7/7 jvm tests pass.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:39 +00:00
Claude e876e2b09b feat(zap-splits): hide single-author zap split row in NoteCompose
When a note's only zap split recipient is the post author, the split is
redundant — the author already receives the zap. Skip rendering the row
in those cases by gating on a new `hasZapSplitSetupBesidesAuthor` helper.
2026-05-27 14:54:48 +00:00
m daa7c7913e feat(namecoin): mention umbrel alongside StartOS in docs and UI hints
Both umbrelOS (via getumbrel/umbrel-apps#4962) and StartOS / Start9
(via Start9-Community/namecoin-core-startos) ship a self-hosted
Namecoin Core that this backend can target. Generalize the
help/strings so umbrel users discover the feature too.

No logic changes.
2026-05-27 14:50:37 +10:00
m a58e7164b5 feat(namecoin): add Namecoin Core RPC backend with optional ElectrumX fallback
Adds a second resolution backend alongside the existing ElectrumX path:
users can now point Amethyst directly at a Namecoin Core full node
(e.g. a StartOS / Start9 installation) instead of (or in addition to)
trusting public ElectrumX operators.

Settings -> Namecoin grows three new pieces:

  1. Backend selector (radio) - ElectrumX | Namecoin Core RPC
  2. Core RPC section - URL, username, password, masked password,
     'Test RPC' button that calls getblockchaininfo and reports
     chain / height / sync %, error path with diagnostic message
  3. Fallback policy - independent toggles for falling back to the
     user's custom ElectrumX servers (Core RPC primary only) and/or
     the hardcoded public ElectrumX defaults

Quartz additions:
  - NamecoinBackend enum, NamecoinCoreRpcConfig (kotlinx.serialization),
    NamecoinFallbackPolicy
  - NamecoinNameBackend interface + ElectrumxNameBackend adapter +
    CompositeNamecoinBackend orchestrator (implements IElectrumXClient
    so NamecoinNameResolver is unchanged)
  - NamecoinCoreRpcClient (jvmAndroid) - JSON-RPC name_show /
    getblockchaininfo over OkHttp, reuses
    roleBasedHttpClientBuilder.okHttpClientForNip05() so Tor onion
    endpoints work without extra plumbing

Semantics:
  - Authoritative negatives (NameNotFound, NameExpired) short-circuit
    the chain - no silent privacy leak to other backends
  - Only transport / unreachable failures cascade through the chain
  - All fallback toggles default off (custom servers stay exclusive,
    matching existing behaviour)
  - Settings persisted via NamecoinSharedPreferences DataStore
  - HTTP transport delegated to roleBasedHttpClientBuilder so existing
    Tor/proxy/cert pinning all works for Core RPC too

Tests:
  - CompositeNamecoinBackendTest (8 cases) - short-circuit, cascade,
    authoritative-negative, electrumx-primary path, expired-name,
    random-exception-cascade
  - NamecoinCoreRpcClientTest (7 cases) - success parsing, auth header,
    name-not-found, expired, generic RPC errors, unusable config,
    probe success + auth failure
  - NamecoinSettingsTest (8 cases) - parser plus new backend / fallback
    fields

Builds clean: :amethyst:compileFdroidDebugKotlin, :quartz:jvmTest.
2026-05-27 14:03:47 +10:00
Vitor Pamplona 5fce6764b5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-26 18:48:06 -04:00
Vitor Pamplona 2dd0166fee Better checks the id and sig before verifying the event. 2026-05-26 18:42:12 -04:00
Claude d1749c314f fix: audit findings on iOS-readiness migration
Address bugs and gaps surfaced by an audit of the prior 14 commits.
JVM tests passed because of typealias / platform-type lenience that
won't hold on Native; these are real iOS compile / behavior issues.

BUG fixes (iOS compile failures):

- commons/.../Note.kt:899 — Iterable.sumOf { -> BigDecimal } is a
  JVM-stdlib-only overload. Common stdlib ships sumOf only for
  Int/Long/Double/Float/UInt/ULong. Replaced with fold(BigDecimal(0)).
- commons/.../Note.kt:889 — BigDecimal(it.event?.content): the quartz
  expect-class constructor takes String non-null; JVM accepted nullable
  via platform-type lenience and threw NPE caught downstream. Switched
  to ?.let { content -> BigDecimal(content) }.
- commons/.../Note.kt:838 — `catch (e: java.lang.Exception)` -> `Exception`.
- commons/.../feeds/custom/FeedDefinitionBuilder.kt + FeedBuilderState.kt:
  inline FQN `java.util.UUID.randomUUID().toString()` -> kotlin.uuid.Uuid.
  random().toString() (Kotlin 2.0+, @OptIn ExperimentalUuidApi).
  inline `System.currentTimeMillis() / 1000` -> TimeUtils.now() (already
  used elsewhere in the codebase).
- commons/.../viewmodels/NestViewModelTest.kt: moved from commonTest to
  jvmTest. The test imports NestViewModel + nestsclient, both of which
  the prior PR moved to jvmAndroid. commonTest depends on commonMain
  only, so the test would fail to compile for iosSimulatorArm64Test.

SUBTLE fixes:

- commons/.../UserRelaysCache.kt: the flow field used double-checked
  locking on a non-volatile var. JMM hazard on Native (ARM weak memory
  model) — outer fast-path could observe a partially-published
  WeakReference. Added @kotlin.concurrent.Volatile.
- commons/.../util/UrlValidation.ios.kt: NSURL.URLWithString("http:")
  returns non-null with scheme="http" and no host; JVM's URI.toURL()
  rejects with MalformedURLException. Reject scheme-only network URLs
  (http/https/ws/wss/ftp without a host) to match JVM behavior.
- commons/.../util/KmpLock.kt commonMain doc: corrected "NSLock" ->
  "NSRecursiveLock" to match the actual iOS implementation.

verifyKmpPurity gate extended (commons + quartz):

- Adds patterns: System.currentTimeMillis, Thread.sleep, java.util.UUID,
  kotlin.jvm.Synchronized, kotlin.jvm.Volatile.
- Each pattern paired with a hint pointing at the canonical KMP
  replacement; the error message surfaces both.
- Skips lines that start with //, *, or /* to avoid false positives on
  KDoc / migration notes.
2026-05-25 02:17:32 +00:00
Claude 880c1bfd4a refactor: clear final java.* imports from commons/commonMain
Three changes that bring commons/commonMain to zero java.* imports
(down from 18 at the start of Phase 2).

- EventListMatchingFilter, NoteListMatchingFilter: moved to jvmAndroid.
  Both use ConcurrentSkipListSet + SortedSet for ordered concurrent
  iteration, and their only consumer is LocalCache in the Android app.
  iOS-time we can revisit if a KMP ordered concurrent set is needed.

- Note.kt's BigDecimal: switch import from java.math.BigDecimal to
  quartz's existing expect/actual com.vitorpamplona.quartz.utils.BigDecimal.
  BigDecimal.ZERO -> BigDecimal(0); BigDecimal.valueOf(longVal) ->
  BigDecimal(longVal) (the expect class already has the Long
  constructor). NoteOnchainZapTest gets the same treatment.

- Adds two top-level extensions in quartz commonMain (separate
  BigDecimalOps.kt file to avoid the duplicate-JVM-classname collision
  with the existing BigDecimal.kt actuals):
    operator fun BigDecimal.plus(other: BigDecimal)
    operator fun BigDecimal.minus(other: BigDecimal)
  Lets += / + / - continue to work on commonMain BigDecimal values.

Commons/commonMain is now structurally iOS-ready as far as the
java.* import audit can tell. Remaining iOS work: actually flip on
the iOS targets, see what UI / dep transitives break, and address.
2026-05-24 23:48:29 +00:00
Claude e0c3b18731 ci: add iOS test job for quartz + commonMain purity gate
Phase 1 of the iOS support plan (amethyst/plans/2026-05-24-ios-support.md).
Two independent guards so JVM-only imports can't silently appear in
quartz's iOS-bound source sets:

- :quartz:verifyKmpPurity (Linux, ~1s): scans commonMain + apple/native
  source sets for com.fasterxml.jackson / okhttp3 references and fails
  the build with a clear pointer to the offending file:line. Wired into
  the existing lint job so it runs on every PR.

- test-quartz-ios (macos-latest): runs :quartz:iosSimulatorArm64Test on
  the simulator (NIP-04, NIP-17, NIP-19, NIP-49 vectors + AES-GCM and
  chatroom-key tests already in quartz/src/iosTest) and additionally
  compileTestKotlinIosArm64 to catch device-variant compile drift.
2026-05-24 16:12:34 +00:00
Claude 9ac32a3a8e feat(nip82): software application visualization + apps feed
Wire NIP-82 Software Applications (kind 32267), Releases (kind 30063)
and Assets (kind 3063) into the Quartz event model and surface them
through a dedicated rendering path in Amethyst.

Quartz: extend the existing experimental NIP-82 builders with topic
(`t`) and NIP-34 app-link (`a`) helpers, and add a small detector
(`isNip82SoftwareRelease`/`asSoftwareRelease`) so kind 30063 events
can be disambiguated from NIP-51 ReleaseArtifactSetEvent at the
renderer layer. Pin behavior with unit tests covering build paths,
disambiguation, and the real-world Amethyst NIP-82 description event.

Amethyst: add modern card visualizations for each kind — application
header with icon/screenshots/platforms/topics/links, release header
with channel pill and bundled-asset count, and asset row with MIME,
size and platforms — and dispatch to them from both `NoteCompose`
and `NoteMaster` (`ThreadFeedView`).

A new "Apps" feed (left nav drawer) mirrors the Picture Feeds shape:
`SoftwareAppsFeedFilter` reads kind 32267 from `LocalCache`, a
`PerUserEoseManager`-backed subscription pulls applications and
releases from outbox relays, and a dedicated screen renders them in
a `LazyColumn` of `RenderSoftwareApplication` cards.
2026-05-21 21:10:18 +00:00
Vitor Pamplona e0abc4a224 v1.11.0 2026-05-20 18:59:05 -04:00
Claude 45aa6044b7 fix: on-chain zap splits — drop sender from splits, merge duplicates, gate Send on dust
Audit findings from an independent code review:

- HIGH: When the user zaps their own post (a common flow), every split
  that included the post author put the sender on the recipient list,
  and OnchainZapBuilder.buildSplit refused the whole tx with "cannot
  zap yourself". Fix: new OnchainZapSplitter.prepare() filters the
  sender's pubkey out of the splits before they reach the builder.
- HIGH: NIP-57 lets the same pubkey appear in zap-split tags more than
  once (additive weights). buildSplit rejected duplicate recipients.
  Same prepare() helper merges duplicates by summing weights, in
  first-seen order.
- HIGH: The dialog's live preview only showed amounts for recipients
  whose share was BELOW dust (because DustRecipientException only
  carries belowDust). Fix: parent composable computes shares with a
  zero dust threshold for the preview, gating the Send button on a
  separate belowDustShares check so the user can see all amounts and
  can't tap Send into a guaranteed BUILDING-stage failure.
- MEDIUM: OnchainZapSendResult.Failure didn't carry the ids of
  receipts that successfully published before a partial-publish
  failure. Added publishedReceiptEventIds: List<HexKey>.
- LOW: useSplits state was keyed by zappedEvent reference; re-emitted
  bundles would silently reset the toggle. Now keyed on the event id.

Tests added:
- splitter: prepare() drops sender, merges duplicates, filters
  non-positive weights; floating-point weights (0.1 + 0.2) sum exactly
- builder: buildSplit produces N recipient outputs + 1 change at index
  N, conserves sats, rejects duplicates and below-dust shares
- sender: sendSplit publishes one receipt per recipient sharing the
  txid with correct per-recipient amount; partial-publish failure
  carries the broadcast txid and the ids of receipts that did publish
2026-05-20 20:14:44 +00:00
Claude 3ed2245d8c feat: on-chain zap splits
Extends NIP-BC onchain zaps to honor a note's NIP-57 zap-split tags: one
Bitcoin transaction pays every pubkey-based recipient atomically, and
one kind:8333 receipt is published per recipient (each receipt carries
the recipient's pubkey + sat share and shares the same i:<txid>).

quartz / OnchainZapBuilder
- new buildSplit(recipients = listOf(pubkey to sats), ...) produces a
  PSBT with one output per recipient + optional change output
- existing build(...) now delegates to buildSplit; coin selection and
  change-vs-dust logic are unchanged for the single-recipient path

commons / new OnchainZapSplitter
- distribute(totalSats, splits, dustThreshold) does the weighted
  integer-math allocation, dropping the rounding remainder onto the
  largest-weight recipient first so the per-recipient sats sum exactly
  to totalSats
- throws DustRecipientException if any share lands below dust; the
  caller surfaces that as a build-stage failure before the tx is built
- unit tests cover equal weights, fractional weights, remainder
  distribution, dust rejection, and input-order preservation

commons / OnchainZapSender.sendSplit
- mirrors send() but takes the precomputed shares, builds via
  buildSplit, and publishes N receipts using the same txid; if one
  receipt publish fails the broadcast txid + already-published receipt
  ids are surfaced in the Failure result

amethyst / Account.sendOnchainZapWithSplits
- thin wrapper that hands off to OnchainZapSender.sendSplit using the
  signer's pubkey

amethyst / OnchainZapSendDialog
- detects pubkey-based zap splits on the zappedEvent and, when present,
  defaults to split mode: a SplitsRecipientSection renders one row per
  recipient with weight % and live per-recipient sats preview
- lnAddress-only splits are filtered out (no pubkey -> no Taproot
  address); a short note tells the user how many recipients were
  skipped
- the send button label switches to "Send X sats, N ways"; an opt-out
  button lets the user fall back to single-recipient mode
- on send: shares are recomputed via OnchainZapSplitter; below-dust
  configurations surface as a BUILDING-stage failure before signing
2026-05-20 19:55:15 +00:00
Claude 6a0801427b Revert "Merge pull request #2990 from vitorpamplona/claude/add-i2p-privacy-option-nK2X7"
This reverts commit d42482ff56, reversing
changes made to a8b6766f49.
2026-05-19 23:10:56 +00:00
Vitor PamplonaandGitHub d42482ff56 Merge pull request #2990 from vitorpamplona/claude/add-i2p-privacy-option-nK2X7
Add I2P support with unified privacy routing
2026-05-19 16:56:54 -04:00
Claude 24cf3fdc5f docs: update onchain-zap + headers-explorer plans for NIP-BC inline SPV tags
nostr-protocol/nips#2332 adds optional ["block", …] and ["proof", …] tags
on kind:8333 that ship the SPV proof inline. The data-model side is already
shipped in Quartz (BlockTag, ProofTag, OnchainZapEvent.block()/.proof() and
the TagArray helpers), but no production code path produces or consumes
either tag yet.

amethyst/plans/2026-05-14-onchain-zaps.md — onchain zaps plan:
- Update the "Chain backend" decision bullet to flag the spec change and
  the two production gaps (send-side emit, receive-side consume).
- Add a dedicated "Inline SPV proofs" section covering: spec status and
  the merkle-proof encoding ambiguity flagged back to the PR; per-layer
  status matrix (shipped vs gap, with file locations); send-side
  two-publish design (Design B — keep instant receipt, add post-confirm
  republish with block+proof; dedupe by (txid, target)); receive-side
  fast-path with fall-through on proof failure (never hard-reject);
  phased delivery G.1–G.7 with effort estimates (~3–4 d after S1 ships
  and spec encoding lands).
- Add the two new pending items to the existing "What's still pending"
  list to keep that section authoritative.

quartz/plans/2026-05-08-local-headers-explorer.md — headers-explorer plan:
- Rewrite §19 (follow-up onchain-zap verification) to reflect the spec
  change. Original section assumed we'd need BIP-37 merkleblock or
  full-block fetch over P2P; with the proof inline none of that
  infrastructure is required. Estimate collapses from 10–15 d to ~3–4 d.
- Point §19 at the full implementation plan in the onchain-zaps file,
  keeping S1 focused on OTS while the follow-up details live with the
  rest of the NIP-BC work.
2026-05-19 19:17:36 +00:00
Claude d19bcf07b0 docs(quartz): add interop-test-vectors section to local-headers-explorer plan
Every consensus-relevant layer of the planned headers explorer (BlockHeader80
parser, DifficultyTarget compact↔target, CalculateNextWorkRequired retarget,
MedianTimePast, header validator end-to-end, P2P wire codecs, reorg/chain
selection, OTS proofs) is pinned to upstream test vectors committed under
quartz/src/commonTest/resources/bitcoin/, matching the existing
nip44.vectors.json / bip39.vectors.json / mls/*.json pattern.

The single highest-value test is a nightly differential check asserting
LocalHeadersBitcoinExplorer.blockHash(h) == OkHttpBitcoinExplorer.blockHash(h)
for every height in [checkpoint, tip] — any consensus drift surfaces as a
disagreeing height.

Maps cleanly onto the existing Phase 1/3/4/5/7/9 work, adding ~5–7
engineer-days total to the plan budget. No new top-level phase needed.
2026-05-19 18:35:30 +00:00
Claude f3637dd7c1 docs(quartz): revise local-headers-explorer plan — SQLite, no bundle, OTS-only
Lock the design choices from the 2026-05-19 review against the intervening
2026-05-14 onchain-zaps work:

- Move the module from quartz/.../nip03Timestamp/bitcoin/ to a sibling
  quartz/.../bitcoin/ package so the headers explorer, header validator,
  peer pool and store can be reused by the future onchain-zap merkle-proof
  work without an inverted import path.
- Use androidx.sqlite + BundledSQLiteDriver for HeaderStore, matching the
  existing SQLiteEventStore. Schema lives in commonMain via IModule. Drops
  the hand-rolled flat-file + sidecar height index.
- Drop the bundled headers blob. Ship a single hardcoded PinnedCheckpoint
  constant; first-run sync starts from the checkpoint and pulls forward
  over P2P. APK growth: 0 bytes.
- Pre-checkpoint OTS heights fall through to OkHttpBitcoinExplorer via
  BitcoinExplorerEndpoint (shared with the onchain-zap EsploraBackend).
  Strict-mode users get an explicit error instead of a network call.
- Mark trustless NIP-BC onchain-zap verification as out of scope and
  capture it as a follow-up plan (BIP-37 merkleblock or full-block fetch
  on top of this stack).

Resolves open questions Q1, Q2 and Q4 from the original plan; Q3 (Quartz
public API vs internal) left open for Phase 0.
2026-05-19 18:14:30 +00:00
Claude 4346427e9f Validate zap receipts against LNURL provider's nostrPubkey (NIP-57 Appendix F)
Receipts were only being checked for a valid event signature — anyone could
sign a kind:9735 and have it counted toward another user's zap totals. NIP-57
Appendix F mandates three additional checks: receipt.pubkey == LNURL
provider's nostrPubkey (MUST), bolt11 invoice amount == zap request "amount"
tag (MUST), and lnurl tag == recipient's lnurl (SHOULD).

- Adds LnZapReceiptValidator + LnurlForm in quartz commonMain (pure logic).
- Adds LnurlEndpointCache (jvmAndroid) and the LnurlEndpointResolver
  interface for async lookup. The cache is primed by outbound zaps (existing
  LightningAddressResolver fetches now extract nostrPubkey) and on demand for
  inbound receipts when no entry is present.
- Adds OkHttpLnurlEndpointResolver in commons, wired into LocalCache via
  AppModules using the existing money-tier OkHttp builder (so Tor settings
  apply).
- LocalCache.consume(LnZapEvent) now: drops receipts that fail MUST checks
  synchronously when the cache is warm, defers credit until async resolution
  finishes on cache miss, and falls back to legacy signature-only behavior
  when no resolver is wired (tests).
- LnZapRequestEvent.create() now accepts amountMillisats + lnurl; both are
  threaded through Account.createZapRequestFor and emitted as tags so future
  receipts can be validated against them.

21 new tests cover validator reasons, lnurl form canonicalization across
lud16/URL/bech32, and cache eviction.
2026-05-19 16:19:29 +00:00
Vitor PamplonaandGitHub 1afa86cb26 Merge pull request #2974 from vitorpamplona/claude/clickable-wallet-card-hHTbM
Add on-chain transaction history screen with pagination
2026-05-19 10:35:31 -04:00
Vitor PamplonaandClaude Opus 4.7 ab3d1dd7e5 fix(quartz/sqlite): set busy_timeout to deflake reader+writer races
Without busy_timeout SQLite returns SQLITE_BUSY immediately when
BEGIN IMMEDIATE can't acquire a lock — e.g. during a WAL
auto-checkpoint or a reader briefly upgrading its snapshot — instead
of retrying. ParallelInsertTest's reader+writer test hit this ~10%
of runs. 5s matches Room's default and adds no overhead in the
uncontended case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:02:35 -04:00
Vitor PamplonaandGitHub 2fff2c137c Merge pull request #2968 from vitorpamplona/claude/fix-image-loading-layout-DlLYr
Support floating-point dimensions in NIP-92 imeta tags
2026-05-18 17:43:29 -04:00
Claude 669199ab7e refactor(quartz): use LargeCache for RelayAuthenticator authStatus
#2946 fixed the ClassCastException with a bespoke AtomicReference<Map>
+ CAS copy-on-write helper. Quartz already has a concurrent-map
abstraction for exactly this purpose — LargeCache — with platform-tuned
actuals (ConcurrentSkipListMap on jvmAndroid, CacheMap on Apple, custom
on Linux). Swap to it.

Removes the bespoke putAuthStatus/removeAuthStatus helpers, the
ExperimentalAtomicApi opt-in, and the AtomicReference imports.

The RelayAuthenticatorConcurrencyTest from #2946 still passes against
the new implementation.
2026-05-18 21:21:02 +00:00
davotoula fcf704a3c6 fix(quartz): make RelayAuthenticator authStatus thread-safe (#2946)
OkHttp dispatches WebSocket callbacks on one thread per relay socket,
so RelayAuthenticator's plain LinkedHashMap was mutated concurrently
from many threads during connection storms. When a bucket crossed
HashMap's TREEIFY_THRESHOLD the racing treeify corrupted internal
state and threw ClassCastException: LinkedHashMap$Entry cannot be
cast to HashMap$TreeNode from onDisconnected.
2026-05-18 18:34:17 +02:00
Claude e719d9ef00 fix(imeta): accept floating-point dimensions so image space is reserved pre-load
Primal-style clients emit "dim 317.0x498.0" in NIP-92 imeta tags. DimensionTag.parse
called Int.parseInt on each component, threw NumberFormatException, and returned
null. With dim==null and no cached aspect ratio, GifVideoView / UrlImageView built
the container without an aspectRatio modifier, so the inline image collapsed to
zero height and the post body looked empty until Coil delivered the bitmap.

Parse each component as Double then truncate to Int. Adds DimensionTagTest covering
integer, float, truncation, 0x0 and malformed inputs.

https://claude.ai/code/session_01W1crao6Hwip8k5ByoLxVrc
2026-05-17 23:22:10 +00:00
m 968a396779 feat(electrumx): add electrum.nmc.ethicnology.com to default server set
Adds a third public Namecoin ElectrumX server to the default and
Tor-preferred lists in DEFAULT_ELECTRUMX_SERVERS / TOR_ELECTRUMX_SERVERS:

  electrum.nmc.ethicnology.com:50002  (IPv4 142.44.246.181, OVH Canada)

Operated by @ethicnology, who ships the namecoind + ElectrumX + mempool
podman stack at github.com/ethicnology/namecoin-compose. Probed live:

  - server.version      -> ElectrumX 1.19.0, protocol 1.4
  - server.features     -> Namecoin mainnet genesis 000000000062b72c...c770
  - scripthash.get_history for d/testls -> full history (heights up to
    822885), and blockchain.transaction.get decodes the OP_NAME_UPDATE
    output correctly. Same code path used by ElectrumXClient against all
    other public servers, no client changes required.

TLS uses a publicly-trusted Let's Encrypt cert, so usePinnedTrustStore
is left at the default (false). This makes it the first entry in the
list whose TLS does NOT depend on PINNED_ELECTRUMX_CERTS, and adds
useful diversity:

  - electrumx.testls.space      (self-signed, pinned, often ECONNRESETs)
  - nmc2.bitcoins.sk / 46.229.238.187  (self-signed, pinned)
  - relay.testls.bit / 23.158.233.10   (self-signed, pinned)
  - electrum.nmc.ethicnology.com       (LE cert, system trust store)

If every self-signed peer is unreachable (e.g. corporate networks that
strip unknown CAs but allow LE chains), resolution can still succeed.

No bare-IP companion entry is added for 142.44.246.181: unlike the
46.229.238.187 / 23.158.233.10 pinned peers (which use DER-SHA256
pinning that ignores hostname verification), an IP-literal endpoint
against the LE cert would fail standard hostname verification under
the system trust manager (SAN covers only the hostname). The IP is
captured in this commit message and the source comment for reference.

Verification on this branch:
  - :quartz:spotlessCheck   OK
  - :quartz:jvmTest         OK (BitRelayResolverTest etc. unchanged)
2026-05-17 17:09:42 +10:00
Claude 8ffe1aee4f feat(wallet): clickable onchain card opens transaction history
Tapping the Bitcoin card on the wallet screen now navigates to a new
OnchainTransactionsScreen that lists transactions touching the account's
Taproot address, mirroring the NWC transactions view.

- OnchainBackend gains getTxsForAddress(address, afterTxid) returning
  BitcoinAddressTx rows (netValueSats, confirmations, blockHeight,
  blockTime, counterparty addresses). EsploraBackend implements it via
  GET /address/{addr}/txs and /address/{addr}/txs/chain/{last_seen}
  for pagination; CachingOnchainBackend passes through.
- OnchainTransactionsViewModel loads the address from the account
  signer + LocalCache.onchainBackend, paginates, and for each chain
  row scans LocalCache for an OnchainZapEvent with a matching txid so
  the UI can render the Nostr counterparty (sender pubkey for
  incoming, p-tagged recipient for outgoing).
- ALL / ZAPS / NON-ZAPS filter chips reuse the existing
  TransactionFilter enum. Mempool rows are flagged "Pending" in
  bitcoin-orange.
2026-05-16 22:19:47 +00:00
Vitor Pamplona 280f21159f v1.10.0 2026-05-16 16:53:00 -04:00
Claude fb2f05d9cb feat: scaffold I2P as a parallel privacy transport to Tor
Foundational types for offering I2P alongside the existing internal Tor.
No wiring yet — HTTP managers, RoleBasedHttpClientBuilder, the Android
I2P service and the Privacy settings UI follow in later commits.

Quartz (relay URL classifier):
- Add isI2p() and classifyHidden() to RelayUrlNormalizer
- Add HiddenServiceKind { CLEARNET, LOCALHOST, ONION, I2P }
- Add NormalizedRelayUrl.isI2p() / classifyHidden() extensions
- Extend the scheme-default branch so .i2p hosts default to ws:// like .onion

Commons (transport-agnostic types):
- PrivacyTransport enum { DIRECT, TOR, I2P }
- TransportChoice (UI-facing per-feature picker, screen-coded for persistence)
- FeatureRole + FeatureTransportChoices: per-feature picks for clearnet traffic
- PrivacySettings aggregate { tor, i2p, features }
- PrivacyRouter.route(url, role, settings): hostname pin for hidden services,
  per-feature choice for clearnet, downgrades to DIRECT if backing transport is OFF

Commons (I2P settings model, mirrors tor/):
- I2pSettings, I2pType (OFF/INTERNAL/EXTERNAL), I2pRelaySettings
- I2pRelayEvaluation, I2pServiceStatus
- II2pManager, II2pSettingsPersistence (platform-agnostic interfaces)
- PrivacyRelayEvaluation composing TorRelayEvaluation + I2pRelayEvaluation

Tests:
- PrivacyRouterTest covers localhost bypass, onion pin, i2p pin, hostname-wins-over-picker,
  per-feature picks routing independently, downgrade-when-transport-OFF, .b32.i2p
2026-05-16 19:37:06 +00:00
Claude 8a5d0019c1 Merge remote-tracking branch 'origin/main' into claude/nip88-polls-quartz-p5fBa 2026-05-16 19:15:32 +00:00
Vitor Pamplona 8a498695a9 v1.09.2 2026-05-16 12:06:32 -04:00
Vitor Pamplona 32b9a06612 Ignores duplicated hashtags in different char cases when processing hashtag spam 2026-05-16 11:34:11 -04:00
Vitor Pamplona 5a30b10a77 v1.09.1 2026-05-15 18:25:17 -04:00
Vitor Pamplona 74c0d6906e v1.09.0 2026-05-15 15:43:47 -04:00
davotoula 8d225c355e Code review:
- reject ill-formed localpart dots and IP literals in parse
- avoid Pair allocation in Nip05Id.parse
2026-05-15 17:17:43 +02:00
davotoula e79755dab3 fix(nip05): stop reporting verification errors as Verified 2026-05-15 17:17:43 +02:00
Claude d77f2f7333 docs(onchain-zaps): record the hand-rolled Bitcoin consensus code decision
Decision: keep the hand-rolled psbt/ + taproot/ consensus layer rather than
adopting fr.acinq.bitcoin-kmp. Rationale: a deliberately small single-key-path
P2TR subset, pinned to authoritative BIP-341/350 test vectors at every layer
(sighash, tweak, witness signature bytes, addresses, tx serialization), and
consistent with the project's minimal-dependency stance.

Recorded in amethyst/plans/2026-05-14-onchain-zaps.md with the consequence
spelled out (we own correctness; revisit if scope expands past single-key-path
P2TR). The psbt/ and taproot/ packages now carry a pointer back to that
decision so a future reader doesn't reflexively swap in a library.

Doc-comment + plan-doc only; no logic change.
2026-05-14 18:16:30 +00:00
Vitor Pamplona 76c7c1575e removes incorrect test 2026-05-14 13:13:51 -04:00
Vitor Pamplona 4a71d890cb Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-14 13:13:39 -04:00
Vitor Pamplona 5d060899c2 additional linting 2026-05-14 12:11:39 -04:00
Claude b7ead41145 fix: use lambda for generic RootIdentifierTag.match call
RootIdentifierTag is generic, so the `::match` method reference failed
JVM compilation. Call it through a lambda like the rest of CommentEvent.

https://claude.ai/code/session_01ArHkNXu1ANrVGZAyMWg4Xu
2026-05-14 15:28:53 +00:00
Claude a690777ef6 feat: show external-id scope as reply context for NIP-22 comments
A Comment event (kind 1111) scoped to an external identifier (`I` tag)
previously rendered as a bare text post and landed in the Home "New
Threads" feed. Treat it as a reply to that external scope: it now shows
in the conversations feed and renders a typed chip (hashtag, geohash,
url, or generic) above the comment text.

https://claude.ai/code/session_01ArHkNXu1ANrVGZAyMWg4Xu
2026-05-14 15:05:04 +00:00
Claude db6254872e fix(onchain-zaps): recover the OnchainZapBuilder package + re-audit fixes
The big one: the source package was named `nipBCOnchainZaps/build/`, which
the repo .gitignore (`build/`) silently matched — so OnchainZapBuilder.kt
(the main source, not just its test) was NEVER committed. The pushed branch
did not compile; the pre-commit hook only checks the working tree, so this
went unnoticed. Renamed the package `build` -> `builder` (a source package
must never be named `build`) and updated all references; the recovered files
are now actually tracked.

Re-audit findings on the previous fix commit:
- CachingOnchainBackend.txCache was unbounded -> memory leak in a long
  session. Added a bounded cache (maxCachedTxs, oldest-entry eviction).
- OnchainZapSender still trusted the signer's returned PSBT for witness
  UTXOs and tap internal keys (used to compute the sighash + the verified
  output keys). Now it copies ONLY the PSBT_IN_TAP_KEY_SIG records back onto
  the PSBT we built, so a signer can contribute signatures and nothing else;
  verification and finalization run entirely on our own PSBT.

Test coverage added:
- OnchainZapBuilderTest: confirmed-UTXO filter — unconfirmed UTXOs excluded
  by default, spendable only with allowUnconfirmed, confirmed preferred.
- CachingOnchainBackendTest: confirmed-tx cached forever, unconfirmed
  re-fetched after TTL, not-found never cached, tip/fee TTL, bounded
  eviction.

All quartz / commons jvmTest suites pass; quartz android + amethyst compile.
2026-05-14 13:55:18 +00:00
Claude 1d7be1d444 fix(onchain-zaps): address audit findings — fund-safety, interop, robustness
CRITICAL
- PsbtSignatureVerifier: independently verifies every key-path signature in a
  PSBT (BIP-340 sig over the BIP-341 sighash, against the tweaked output key).
- OnchainZapSender now rejects a signer that returns a different transaction
  than it was asked to sign (byte-compares the unsigned tx) and verifies all
  signatures before broadcasting — closes a substitution attack where a
  malicious/buggy external signer could redirect funds.

HIGH — break test circularity
- Pin the full BIP-341 keyPathSpending input-0 witness: signing the vector
  sighash with the vector tweaked key reproduces the vector signature
  byte-for-byte (also pins BIP-340 nonce determinism).
- Pin TaprootAddress.fromPubKey + SegwitAddress.encodeP2TR against all seven
  BIP-341 wallet-test-vector P2TR mainnet addresses.
- EsploraBackendTest: JSON-parsing coverage for /tx, /address/{}/utxo, the
  mempool.space and blockstream fee formats, and schema-fallback.

MEDIUM
- OnchainZapBuilder filters to confirmed UTXOs by default (allowUnconfirmed
  opt-in) and signals BIP-125 RBF (nSequence 0xFFFFFFFD).
- CachingOnchainBackend: TTL-caching decorator (confirmed tx forever,
  unconfirmed/tip/fees short TTL) so a feed of onchain zaps doesn't fan out
  into one HTTP request per event. Wired in AppModules.
- OnchainZapVerifier computes real confirmation depth from the chain tip and
  asserts the backend echoed the requested txid.
- EsploraBackend falls back to the standard Esplora /fee-estimates endpoint
  (blockstream.info) when /v1/fees/recommended 404s.

LOW
- BitcoinTransaction.parse caps input/output/witness-item counts to stop a
  hostile varint from triggering a giant pre-allocation.
- OnchainZapEventTest: asserts kind:8333 on-the-wire tag structure against the
  NIP-BC spec.
- alt tag now includes the amount ("Onchain zap: N sats"), matching the spec
  example.

All quartz / commons / androidHostTest suites pass; amethyst compiles.
2026-05-14 13:29:48 +00:00