Commit Graph
2297 Commits
Author SHA1 Message Date
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
Claude 973c2eeff7 feat(music): add Music Track (kind 36787) and Music Playlist (kind 34139)
Adds quartz support for two new addressable Nostr event kinds, modeled after
the NIP-88 poll structure, plus modern Compose renderers wired into both the
feed (NoteCompose) and the thread/master view (ThreadFeedView).

Quartz:
- MusicTrackEvent (36787): title/artist/url and optional album, track_number,
  released, duration, format, bitrate, sample_rate, language, explicit, image,
  video. Each field is a dedicated *Tag class with parse/assemble, plus a
  TagArrayBuilder DSL and a typed build() factory. Auto-emits the "t music"
  hashtag and an NIP-31 alt description.
- MusicPlaylistEvent (34139): title, image, description, ordered "a" track
  references to MusicTrackEvent, plus public/private/collaborative flags.
- Both registered in EventFactory so LocalCache materializes them as typed
  events instead of generic Event.

Amethyst UI:
- MusicTrack.kt: square cover with overlaid play affordance, large title,
  artist row with note icon, meta row (album/track #/release/duration/explicit
  badge), embedded VideoView for audio (or video URL when present) with
  cover thumbnail, lyric/credit content via TranslatableRichTextViewer, and
  topic chips for extra t tags.
- MusicPlaylist.kt: cover with track-count badge, title, count + collaborative
  /private chips, descriptions, and an ordered list of tracks resolved via
  LoadAddressableNote (clickable, falls back to "loading"/"unknown track" for
  missing references). Capped at 25 with a "+N more tracks" footer.
- Track-count strings use <plurals> for correct CLDR pluralization.
- Wired into NoteCompose `when` dispatch and ThreadFeedView header dispatch.
2026-05-27 00:30:34 +00: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
Claude 5f16ebc060 feat(quartz): NIP-BC sign_psbt over NIP-55 external signer (Phase B)
Wires the NIP-55 Android external-signer contract for sign_psbt, so
NostrSignerExternal.signPsbt is real instead of a stub.

- CommandType.SIGN_PSBT ("sign_psbt") — also available in NIP-55 `perms`
  lists, since Permission wraps CommandType directly.
- SignPsbtResult result type.
- SignPsbtQuery (background ContentResolver) + SignPsbtRequest /
  SignPsbtResponse (foreground Intent), modeled on the derive_key
  string-in/string-out shape: the PSBT hex rides the `nostrsigner:` URI,
  the signed (not finalized) PSBT comes back in the `result` field.
- BackgroundRequestHandler.signPsbt / ForegroundRequestHandler.signPsbt.
- NostrSignerExternal.signPsbt now runs the background-then-foreground
  query. Signer apps that predate sign_psbt reply with no `result`, which
  surfaces as CouldNotPerformException — the send dialog shows it as a
  failure ("update your signer").

NIP-46 (NostrSignerRemote) still throws UnsupportedMethodException — the
bunker-side command isn't standardized yet.
2026-05-14 12:50:47 +00:00
Claude a42b1e0f32 feat(quartz): NIP-BC onchain zap send-side foundation (Phase A.2)
Implements the send half of the onchain zaps plan: a minimal, fund-safe
BIP-174 PSBT pipeline plus the OnchainZapBuilder and `NostrSigner.signPsbt`.
Every cryptographic step is validated against the BIP-341 wallet test
vectors.

nipBCOnchainZaps/psbt/:
- BitcoinIO: little-endian Bitcoin consensus byte reader/writer (u16/u32/u64,
  compact-size varint, var-bytes).
- BitcoinTransaction: OutPoint / TxIn / TxOut / transaction model with legacy
  and BIP-144 segwit serialization, segwit-aware parsing, and witness-stripped
  txid. Validated against the genesis coinbase tx and the BIP-341 9-input
  unsigned tx.
- TaprootSigHash: BIP-341 SigMsg + TapSighash tagged hash for key-path spends.
  All six base sighash types plus both ANYONECANPAY variants verified against
  the seven BIP-341 keyPathSpending vectors.
- Psbt: BIP-174 container subset (global unsigned-tx, per-input witness-utxo /
  tap-internal-key / tap-key-sig / sighash-type, per-output tap-internal-key).
  Unknown records round-trip verbatim. Typed accessor extensions.
- PsbtSigner: signs the key-path P2TR inputs a given private key controls —
  derives the BIP-341 tweaked key, computes the all-inputs sighash, produces a
  BIP-340 Schnorr signature. Leaves inputs the key does not control untouched.
- PsbtFinalizer: moves tap-key-sigs into the witness stack, yields a
  broadcastable transaction.

nipBCOnchainZaps/build/:
- OnchainZapBuilder: largest-first coin selection + unsigned-PSBT assembly for
  a NIP-BC zap. Sender spends from / changes back to the single Taproot address
  derived from their Nostr pubkey; recipient output pays the recipient's
  derived address. Dust-aware change handling, InsufficientFundsException,
  self-zap and dust-amount guards.

nipBCOnchainZaps/taproot/:
- TaprootAddress.tweakSecretKey: BIP-341 taproot_tweak_seckey (key-path-only),
  including the odd-y internal-key negation. Validated against the BIP-341
  vector's tweakedPrivkey.

Secp256k1Instance: new `privKeyNegate` primitive across commonMain expect +
jvm / android / native actuals.

Signer hierarchy — new `NostrSigner.signPsbt(psbtHex): String`:
- NostrSignerSync / NostrSignerInternal: real implementation.
- NostrSignerWithClientTag: delegates to the wrapped signer.
- NostrSignerRemote (NIP-46) and NostrSignerExternal (NIP-55): throw
  SignerExceptions.UnsupportedMethodException — the bunker command and the
  Android Intent contract are Phase B, and depend on signer-app support.

Tests: 31 new tests across the psbt/build/signers packages, anchored on the
BIP-341 wallet-test-vectors so the tweak, sighash, signature, and finalized
transaction are all checked against an authoritative source.

Still pending: Phase B (NIP-55 sign_psbt Intent) and Phase D (send UI +
broadcast + publish kind 8333).
2026-05-14 11:29:47 +00:00
Claude 79f1d43581 feat(quartz): NIP-BC onchain zap receive-side foundation
Phase A.1 of the onchain zaps plan (amethyst/plans/2026-05-14-onchain-zaps.md):
the verify/receive path of NIP-BC. Send-side (PSBT, builder, signPsbt) is the
next phase.

Adds:
- nipBCOnchainZaps/taproot/SegwitAddress: BIP-173 / BIP-350 native segwit
  address encoder/decoder, witness v0..v16, on top of the existing Bech32 util.
- nipBCOnchainZaps/taproot/TaprootAddress: BIP-341 key-path-only derivation
  from a Nostr pubkey. Computes the TapTweak tagged hash, applies the additive
  tweak via Secp256k1Instance.pubKeyTweakAdd, and encodes as bc1p... bech32m.
- Secp256k1Instance: new pubKeyTweakAdd primitive wired through commonMain
  expect + jvm / android / native actuals.
- nipBCOnchainZaps/chain: pluggable OnchainBackend interface plus BitcoinTx,
  BitcoinTxOutput, Utxo, FeeEstimates data models.
- nipBCOnchainZaps/chain/EsploraBackend (jvmAndroid): OkHttp-based
  Esplora-compatible client (mempool.space / blockstream.info) covering
  /tx, /address/{addr}/utxo, /tx broadcast, /blocks/tip/height, and
  /v1/fees/recommended.
- nipBCOnchainZaps/verify/OnchainZapVerifier: enforces every NIP-BC client
  rule -- reject self-zap, fetch tx, derive recipient Taproot scriptPubKey,
  sum only outputs paying the recipient (excluding change back to sender),
  cap claimed amount at verified amount, mark unconfirmed as Pending.

Tests (commonTest, runs on jvmTest):
- SegwitAddressTest: BIP-350 known-good vectors for v0/v16 + round-trip for
  v1 (P2TR).
- TaprootAddressTest: BIP-341 wallet-test-vectors tagged hash + tweaked
  output key + scriptPubKey, plus address round-trip and prefix/length
  sanity.
- OnchainZapVerifierTest: confirmed / pending / self-zap / missing tx /
  zero-verified-amount / multi-output-sum / amount-capping with an
  in-memory FakeBackend wired to real TaprootAddress-derived scripts.

Plan: amethyst/plans/2026-05-14-onchain-zaps.md documents the full v1 scope,
the merge into the existing NIP-47 wallet UI, the subscription kind-list
edits, and the Note.zapsAmount fold-in approach for display.
2026-05-14 04:34:30 +00:00
Claude d095dc790d feat(quartz): add NIP-BC onchain zaps (kind 8333)
Implements NIP-BC onchain zap events analogous to NIP-57 lightning
zap receipts but for Bitcoin transactions. The recipient's Nostr
pubkey is used directly as the internal key of a BIP-341 P2TR output.

Mirrors the nip88Polls package structure with a single `zap/` subpackage:
- OnchainZapEvent (kind 8333) with profile / event / addressable-event
  zap builders
- Tags: BitcoinTxIdTag (i = bitcoin:tx:<txid>), AmountTag (sats),
  BlockTag (hash + height), ProofTag (raw-tx + merkle proof) for
  optional SPV verification
- TagArrayExt / TagArrayBuilderExt for parsing and assembly
- EventFactory wired so kind 8333 deserializes into OnchainZapEvent

Reuses standard ETag/ATag/PTag/KindTag from nip01Core. Does not yet
implement client-side transaction verification or PSBT signer methods.
2026-05-14 03:25:44 +00:00
m 2c3d006f05 docs: rename NIP-9A -> NIP-9B in code comments + user strings
The upstream NIP draft for kind:34551 community rules
(nostr-protocol/nips#2331) was renumbered from 9A to 9B per maintainer
feedback that slot 9a is already claimed by the push-notifications
draft (nostr-protocol/nips#2194):

  https://github.com/nostr-protocol/nips/pull/2331#issuecomment-4442813289

This commit updates all human-readable references in the merged
community-rules code:

- 7 Kotlin files in quartz (CommunityRulesEvent, CommunityRulesValidator,
  5 tag classes) — Kdoc comments
- 13 Kotlin files in amethyst (composer, feed filter, rules editor,
  Account, AccountSettings, tests) — code comments + Kdoc
- 1 English strings file (values/strings.xml) — 2 user-facing strings
- 54 translation strings files (values-*-r*/strings.xml) — same two
  strings, untranslated "NIP-9A" token replaced with "NIP-9B"

Kind number (34551), schema, tag names, and behaviour are unchanged.
No public API or DTO field renames. Pure docs/strings.

Verified: :quartz:spotlessCheck, :amethyst:spotlessCheck,
:commons:spotlessCheck all clean.
2026-05-14 09:29:59 +10:00
davotoula e21794e42a Manual testing fixes:
- quartz: Event.threadRootIdOrSelf() now falls back to markedReply()?.eventId
when both markedRoot() and unmarkedRoot() are absent
- amethyst: After upstream PR #2855 split the notifications feed into
notificationsFollowing and notificationsEveryone, the hiddenUsers.flow
collector still only invalidated the original notifications feed. Extends
it to invalidate all three.
- amethyst: CardFeedContentState.refreshSuspended() takes an additive-only
path when lastNotes is populated — it only adds cards for new admissions,
never removing cards for notes that no longer pass the filter. Re-muting
mid-session correctly rejected muted-thread reactions in the filter, but
the existing cards stayed in the UI as "Show Anyway" placeholders. Calls
clear() before invalidateData() on each notification feed so the refresh
hits the full-rebuild branch and removals propagate.
2026-05-13 09:06:53 +02:00
davotoula ff780bcb54 Code review:
- consolidate thread-root resolution
- consolidate mute-state writes
2026-05-13 09:06:53 +02:00
David bbb4e39465 Quartz:
- add mutedThreads/mutedThreadIdSet accessors
- MuteTag sealed interface recognizes EventTag
- add EventTag for NIP-51 mute-list e tags
- MuteListEvent round-trip and legacy-tag migration coverage
2026-05-13 09:06:53 +02:00