Commit Graph
14171 Commits
Author SHA1 Message Date
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
Vitor PamplonaandGitHub bbc7f9740e Merge pull request #3065 from vitorpamplona/claude/gifted-ritchie-5XjZf
Exclude author from zap split display logic
2026-05-27 11:09:29 -04:00
Vitor PamplonaandGitHub 7d6bbac200 Merge pull request #3064 from vitorpamplona/claude/sweet-maxwell-UMahG
Add playback error overlay with browser fallback for video codec failures
2026-05-27 11:04:09 -04: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
Claude 89607376cc feat(playback): surface unsupported codec errors with browser fallback
ExoPlayer entered the ERROR state silently when a codec was missing or the
container/format wasn't supported, leaving a blank video area with no
recourse. Track the player error in MediaControllerState, render an overlay
with the error code, and offer an "Open in browser" button so the user can
fall back to the system browser for codecs the device can't decode.
2026-05-27 14:51:09 +00:00
Vitor PamplonaandGitHub d381cf9109 Merge pull request #3063 from davotoula/feat/avif-support
Comprehensive AVIF support (#837)
2026-05-27 10:32:03 -04:00
davotoula ef25f8c0e6 test(amethyst): instrumented coverage for AVIF upload + decode
Adds 4 instrumented test files + 3 tiny pre-committed AVIF fixtures to
catch regressions in the upload pipeline.
2026-05-27 16:11:20 +02:00
davotoula f8b24c645a fix(chat): hide DM quality slider for AVIF and correct error framing 2026-05-27 16:11:20 +02:00
davotoula 7d580452e4 fix(uploads): surface specific AVIF metadata error instead of 'Upload cancelled' 2026-05-27 16:11:20 +02:00
davotoula 50a81c35cf Code review:
style(nests): import TimeUtils in CreateNestViewModel instead of inline FQN

HIGH-1: import java.io.RandomAccessFile in MetadataStripper instead of
inline fully-qualified name

HIGH-2: catch AvifMetadataNotVerifiableException in the 6 ViewModels
that call MetadataStripper.strip directly (profile picture, emoji pack
list+display, bookmark group, nest, channel)

MEDIUM-1: tighten AvifAnimatedDecoderFactory.createAnimatedImageDecoder
annotation from @RequiresApi(P) to @RequiresApi(S); the outer guard is
already SDK_INT < S.

MEDIUM-2: replace the curried lambda DI seam in MetadataStripper with
a named fun interface (AvifExifReader).

MEDIUM-3: rename isGifUrl -> isAnimatedMediaUrl (MyAsyncImage) and
BaseMediaContent.isGif() -> isAnimatedMedia() (ZoomableContentView)
since both predicates now cover AVIF as well as GIF.

- AvifAnimatedDecoderFactory.isAvif now iterates a single brand list
  with .any { rangeEquals(8, it) } instead of three || branches.
- MetadataStripper.inspectAvifMetadata dropped the outer defensive
  try/catch; the inner catch already converts parse failures to
  AvifMetadataNotVerifiableException and the rest of the function
  cannot realistically throw.
- PreviewMetadataCalculator extracts the shared ImageDecoder allocator
  + exception path from decodeAvifBytes and decodeAvifFromUri into a
  single private decodeAvif(source) helper.
- RobohashFallbackAsyncImage merges its identical Loading and Error
  when branches into one via Kotlin's multi-value branch syntax.
- MediaCompressorTest drops a no-op MockKAnnotations.init(this) call
  and the now-unused import; no @MockK fields exist.
2026-05-27 16:11:20 +02:00
davotoula 03c42f585e AVIF display + thumbnail-cache fixes from manual testing
fix(ui): default avatar contentScale to Crop, not Fit
fix(images): skip thumbnail cache for animated AVIF profile pictures
fix(ui): animate profile pictures regardless of URL extension
2026-05-27 16:11:20 +02:00
davotoula 57724cee8c Comprehensive AVIF support (#837)
feat(ui): hide compression slider for non-compressible files (AVIF, GIF, SVG)
feat(images): custom Coil decoder for animated AVIF
feat(ui): include AVIF in animation-aware MIME predicates
fix(uploads): AVIF extension fallback in BlossomUploader
fix(uploads): AVIF extension fallback for NIP-96 multipart filename
feat(uploads): decode AVIF previews with ImageDecoder for blurhash/thumbhash
feat(uploads): fail-closed AVIF metadata inspection in MetadataStripper
fix(uploads): preserve AVIF bytes through MediaCompressor
feat(uploads): add MediaMimeTypes helper for AVIF detection
2026-05-27 16:11:20 +02:00
davotoulaandClaude Opus 4.7 adc0d36407 docs(amethyst): TDD-style implementation plan for AVIF support (issue #837)
15 bite-sized tasks across 6 phases (A foundation, B upload pipeline, C animation
lifecycle audit, D test fixtures + instrumented tests, E manual on-device
verification, F ship). Each task has exact file paths, full test code, full
patch code, exact commands, expected output, and per-task commits.
Companion to amethyst/plans/2026-05-26-avif-support.md spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:11:20 +02:00
davotoula a99927bf92 Docs: plan for comprehensive AVIF support (issue #837)
docs(amethyst): document strip-toggle-off AVIF EXIF leak as known limitation
docs(amethyst): document Desktop AVIF gaps from spot-check
docs(amethyst): record animated AVIF playback caveats from on-device testing
docs(amethyst): note API < 31 gallery-picker greys out AVIF (OS limit)
docs(amethyst): tighten API < 31 known-limitation with on-device findings
docs(amethyst): plan and design for AVIF instrumented tests
2026-05-27 16:11:20 +02:00
Vitor PamplonaandGitHub 1ef8b7405a Merge pull request #3060 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-27 09:56:19 -04:00
Crowdin Bot 72afadbb38 New Crowdin translations by GitHub Action 2026-05-27 13:20:50 +00:00
Vitor PamplonaandGitHub b12e160457 Merge pull request #3056 from mstrofnone/feat/namecoin-core-rpc-backend
feat(namecoin): add Namecoin Core RPC backend with optional ElectrumX fallback
2026-05-27 09:17:30 -04:00
Vitor PamplonaandGitHub 7af34762bb Merge pull request #3062 from vitorpamplona/claude/payment-targets-ui-7pgrY
feat(profile): toast when no app handles a payment target scheme
2026-05-27 09:15:29 -04:00
Claude 023a3c6624 feat(profile): toast when no app handles a payment target scheme
Tapping a chip silently failed if no installed app handled the
type-specific URI scheme (bitcoin:, ethereum:, monero:, etc.).
Surface that case through the existing toastManager so users know
to install a compatible wallet.

https://claude.ai/code/session_01R7kRziq14Hc22dPwAnZRAr
2026-05-27 13:10:36 +00:00
Vitor PamplonaandGitHub 9856458c54 Merge pull request #3061 from vitorpamplona/claude/payment-targets-ui-7pgrY
feat(profile): modern chip layout for payment targets
2026-05-27 09:09:09 -04:00
Vitor PamplonaandGitHub 7e44df0d1f Merge pull request #3059 from davotoula/feat/emoji-pack-add-to-list-menu
Add "Add/remove to/from emoji list" row to pack-card menu
2026-05-27 06:34:10 -04:00
Vitor PamplonaandGitHub da5f01011c Merge pull request #3058 from nrobi144/feat/desktop-profile-editing
feat(desktop): full profile editing — 13 fields, image upload, NIP-05 verification, drag-and-drop
2026-05-27 06:33:26 -04:00
davotoula 205b629c9d Code review:
- tighten EmojiListToggleRow null-handling and label branching
2026-05-27 09:58:26 +02:00
davotoula 142bf67678 Add "Add to emoji list" row to pack-card menu
Closes the UX gap where a user who creates a pack via the in-app UI has no
path to add it to their NIP-51 kind-10030 selection without leaving the
pack-management screens.
2026-05-27 09:58:03 +02:00
nrobi144andClaude Opus 4.6 bcf61d53ff feat(desktop): add drag-and-drop for avatar/banner, fix avatar overlay
- Wire DragAndDropTarget on avatar circle and banner area
- Image-only filter (jpg/png/gif/webp/avif)
- Visual drag-over feedback (primary border highlight)
- Fix avatar: only show placeholder icon when no image set
  (previously overlay was visible behind the loaded avatar)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 10:22:18 +03: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
nrobi144andClaude Opus 4.6 ce173debba fix(desktop): center avatar vertically and increase to 120dp
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 06:51:35 +03:00
nrobi144andClaude Opus 4.6 2e405f5644 fix(desktop): redesign avatar picker as tappable circle with upload overlay
Replace the awkward small icon button with a full 100dp tappable circle.
Shows surfaceVariant background when empty, semi-transparent overlay with
centered upload icon when image is present. Spinner replaces icon during
upload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 06:46:29 +03:00
nrobi144 b608ca02fb Merge remote-tracking branch 'upstream/main' into feat/desktop-profile-editing
# Conflicts:
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt
2026-05-27 06:38:47 +03:00
Claude 1e3d2bbd3d feat(profile): modern chip layout for payment targets
Replace plain text payment-target rows with a FlowRow of pill-shaped
clickable chips that carry a type-aware icon, brand color, uppercase
label, and a truncated address. Tap opens a type-specific URI scheme
(bitcoin:, lightning:, ethereum:, monero:, liquidnetwork:, dash:,
payto:// fallback) so wallets can actually pick up the intent; long-press
still copies the authority to the clipboard.
2026-05-27 02:55:15 +00:00
Vitor PamplonaandGitHub 4306f26460 Merge pull request #3055 from vitorpamplona/claude/affectionate-wright-GRP8r
fix(user-metadata): fall back to indexer relays when outbox is exhausted
2026-05-26 21:55:32 -04:00
Claude db2b9551f9 fix(user-metadata): fall back to indexer relays when outbox is exhausted
If a user's NIP-65 outbox advertises only relays that don't hold their
kind 0, profile fetching used to give up after EOSE on those relays.
filterUserMetadataForKey now widens to the account's indexer relays
once every outbox relay has either EOSE'd or is in cannotConnectRelays
and metadata is still missing. UserWatcherSubAssembler invalidates
filters on EOSE so the fallback re-evaluates without waiting for an
unrelated trigger.
2026-05-27 01:20:37 +00:00
Vitor PamplonaandGitHub f6066de92a Merge pull request #3054 from vitorpamplona/claude/pensive-brown-UVDZf
Replace wallet reorder buttons with drag-and-drop UI
2026-05-26 20:33:59 -04:00
Claude 906ac06c57 feat(wallet): drag-and-drop reorder for NWC wallet cards
Replace the up/down chevron IconButtons on each wallet card with a drag
handle, matching the pattern used across the relay-settings screens.
Reuses RelayDragState / rememberRelayDragState / draggableRelayItem /
relayDragHandle from relays/common — same gesture handling, elevation
animation, and swap-on-threshold behavior.

The handle and item modifier are only attached when there is more than
one wallet to reorder.
2026-05-27 00:10:05 +00:00
Claude 290a6b1f85 fix(nwc): scope Send/Receive/Transactions to the wallet shown in the detail screen
The wallet detail screen's Send, Receive and Transactions buttons navigated to
parameterless routes. Each destination created a fresh WalletViewModel with no
selection, so the action ran against `_defaultWalletId` (the account default)
instead of the wallet being viewed. Paying, invoicing, and listing
transactions could therefore go to the wrong wallet.

Parameterize WalletSend/WalletReceive/WalletTransactions with `walletId`,
plumb it through AppNavigation, pass it from WalletDetailScreen, and have
each screen call `selectWallet(walletId)` before operating.
2026-05-27 00:05:55 +00:00
Vitor Pamplona 5fce6764b5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-26 18:48:06 -04:00
Vitor PamplonaandClaude Opus 4.7 cdb5e01821 fix(nwc): re-add #p to response filter for Alby relay routing
Dropping both `authors` and `#p` from the kind-23195 subscription filter
fixed wallets that don't set those fields the way NIP-47 implies, but
broke purpose-built NWC relays (notably relay.getalby.com/v1) that use
`#p` as the routing key — without it the relay never delivers the
response to our subscription, so the wallet screen sits on a spinner.

Restore `#p: [client pubkey]` in the relay filter. Keep `authors` out
since that field was the one actually causing the broader interop pain.
Spec-compliant responses always carry the `p` tag, so adding it back
does not exclude any conforming wallet. End-to-end authenticity is
still enforced by NIP-04 decryption against the per-connection shared
secret and by the client-side author check in NwcPaymentTracker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:44:14 -04:00
Vitor Pamplona 2dd0166fee Better checks the id and sig before verifying the event. 2026-05-26 18:42:12 -04:00
Vitor PamplonaandGitHub f3ac87689a Merge pull request #3053 from vitorpamplona/claude/tor-stops-working-1PIcU
Add Tor self-heal watchdog + integration tests + Arti v2.3.0
2026-05-26 17:52:54 -04:00
Claude 2c89a62789 test(tor): expand tier-3 to verify each root cause of the wall-and-stop bug
Bug had four ingredients (per the kdoc on TorArtiNativeIntegrationTest).
We had one test for #1; now there's targeted coverage for each:

1) Native TorClient gets stuck (bad guards / dead circuits / expired
   consensus) with no way to drop it in-process:
   `destroy then re-initialize releases the state file lock cleanly`
   (was already there — added exit-IP logging so the developer can eyeball
   that the circuit actually changed across the destroy).

2) In-flight per-connection handlers holding Arc<TorClient> clones,
   pinning the state file lock past destroy:
   NEW `destroy aborts an in-flight SOCKS handler quickly`
   Opens a SOCKS HTTPS request, lets the handler get into the data plane,
   calls destroy() concurrently, asserts:
     - destroy() returns within 3s (the budgeted abort+sleep window),
     - the in-flight request thread dies within 5s,
     - a fresh initialize on the SAME data dir succeeds afterward
       (this is the actual regression net — pre-fix the orphaned handler's
       Arc would keep the TorClient alive and the lock held).

3) stopSocksProxy *deliberately* preserves the running client so the
   legitimate stop/start toggle is fast. We need to keep that path
   working after the destroy/abort changes:
   NEW `stopSocksProxy then startSocksProxy reuses the running TorClient`
   Asserts the second startSocksProxy returns in < 5s — no re-bootstrap.

4) State / fd / memory leaks accumulating across many destroy/init cycles
   (the watchdog can drive these forever):
   NEW `survives multiple destroy then initialize cycles`
   5 full cycles of initialize → startSocksProxy → fetch → destroy.
   Logs per-cycle elapsed time + exit IP so degradation is observable
   even when it's not yet a hard failure.

Plus two extra robustness tests:

NEW `proxies concurrent SOCKS requests in parallel`
   5 in-flight HTTPS-via-SOCKS requests at once. Exercises the Rust
   accept loop, HANDLER_TASKS retain-on-push, and Arc<TorClient> clone
   independence under load.

NEW `destroy is idempotent`
   destroy-without-init, double-destroy, init-after-double-destroy.
   Cheap regression net for unwrap-on-None panics in the Rust shim.

All new tests gated by -Pamethyst.arti.integration=true alongside the
existing ones; the smoke test (`library loads and reports a version`)
still runs unconditionally on Linux x86_64. Total runtime for the slow
suite is ~10-15 minutes against Tor, depending on bootstrap luck.
2026-05-26 21:43:14 +00:00
Claude e39ea55fd6 test(tor): tier-3 integration — JVM host build of Arti, smoke + bootstrap tests
Closes the test gap below the tier-1 unit tests by running the real Arti
JNI shim end-to-end on JVM. Cheaper than an emulator + connectedAndroidTest,
and exercises the exact Rust + JNI code path the Android .so does.

Three tests in TorArtiNativeIntegrationTest:

1. `library loads and reports a version` — always-on smoke check. Loads
   libarti_android.so via System.loadLibrary and calls ArtiNative.getVersion.
   ~10ms. Catches build/link regressions (e.g. a stale .so after an ARTI
   bump, a missing JNI symbol export, a forgotten rebuild on this path).
   Skipped on non-Linux-x86_64 hosts with a clear message pointing at the
   build-arti-host.sh rebuild step.

2. `bootstraps and proxies an HTTPS request through Tor` — opt-in via
   -Pamethyst.arti.integration=true. ArtiNative.initialize → startSocksProxy
   → OkHttp-via-SOCKS → check.torproject.org/api/ip. Asserts "IsTor":true.
   Regression net for the rustls CryptoProvider install we added after the
   v2.3.0 bump and for the destroy/handler-abort fixes in the Rust shim.

3. `destroy then re-initialize releases the state file lock cleanly` — opt-in.
   The direct unit-test mirror of the self-heal path: bootstrap, destroy, hit
   the SAME data dir with initialize again, verify it succeeds without a
   "state file already locked" error and that traffic still flows.

Wiring:
- New tools/arti-build/build-arti-host.sh — companion to build-arti.sh.
  Cargo-builds the wrapper crate for the host target (x86_64-linux on most
  dev machines, but the script maps macOS / arm64-linux too) and copies to
  amethyst/src/test/native-libs/<host-tag>/libarti_android.so.
- amethyst/build.gradle.kts testOptions.unitTests.all configures
  -Djava.library.path so System.loadLibrary("arti_android") finds the
  checked-in host .so. Also forwards -Pamethyst.arti.integration so the
  opt-in gate works from a Gradle invocation.
- Checked-in src/test/native-libs/x86_64-linux/libarti_android.so for the
  most common dev/CI host (~6 MB).

Wrapper change to make the JVM path actually run:
- lib.rs: on #[cfg(not(target_os = "android"))], call
  builder.storage().permissions().dangerously_trust_everyone() so Arti's
  fs-mistrust check doesn't reject /tmp data dirs on hosts where parent
  directories have unusual UIDs (typical in containers). Android keeps its
  strict default — the app's private filesDir is already sandboxed by the OS.
  Compiled-out on Android, so the shipped Android .so is functionally
  unchanged.

Verified in this session:
- Smoke test passes without -P (3 tests, 1 ran, 2 skipped).
- Full unit test suite still passes.
- With -P the bootstrap tests get past Arti's permissions check; they hang
  on actual relay I/O in this container because outbound TCP egress is
  restricted to a CDN allow-list, not Tor relays. Tests succeed on hosts
  with unrestricted outbound — see the test kdoc.
2026-05-26 21:34:26 +00:00
Vitor PamplonaandGitHub f2bfd7a315 Merge pull request #3052 from vitorpamplona/claude/brave-clarke-hJ0PK
onchain zaps + nip-05 filter when returning users to Gemini
2026-05-26 17:22:25 -04:00
Claude 93163141b9 feat(amethyst): anti-impersonation safeguards on AppFunctions write verbs
Zaps and DMs move real artifacts (money, private messages) to a Nostr
pubkey. Nostr has no global namespace, so "zap Alice" is ambiguous —
multiple users can publish the same display name. Four safeguards now
make it much harder for Gemini (or any agent) to misroute a write:

1. `expectedDisplayName: String?` on followUser / sendDm / zapUser.
   Agent passes the name it understood; verb cross-checks that the
   resolved profile's name / display name / NIP-05 contains it (or
   vice-versa). Mismatch aborts with a typed error carrying the npub
   and NIP-05 so the agent can re-prompt.

2. `requireFollow: Boolean = true` default on sendDm and zapUser.
   Refuses to act on a pubkey the user doesn't already follow on
   Nostr. Strongest guard against same-name impersonators — even if
   the agent picked the wrong Alice, the user almost certainly isn't
   following her. Override to false only when the user explicitly
   approves acting on a stranger.

3. Updated kdocs instruct the agent to confirm with the user using
   all three identity signals (display name + npub + NIP-05) before
   invoking. The kdoc is what Gemini reads to learn the verb's
   contract, so this is where the instruction goes.

4. searchProfiles now filters out hits whose NIP-05 claim explicitly
   fails verification (the listed domain refuses to sign for that
   pubkey). Network errors / no-claim profiles are kept (inconclusive,
   not refutations). Verifications run in parallel with a 4s overall
   budget; on timeout we surface all candidates rather than censor.

https://claude.ai/code/session_013NKVhEF2KqyCrV7ufaiQ6N
2026-05-26 21:07:38 +00:00
Claude 3517606b81 test(tor): tier-1 TorManager unit tests + tier-3 instrumented scaffold
Tier 1 — 18 fast unit tests for the self-heal logic, virtual time only:
- Extracted TorBackend interface (status + start/stop/reset/resetWithCleanState),
  TorService implements it. TorManager now takes a TorBackend by injection
  rather than constructing a TorService itself.
- Extracted TorPreferencesPort (torType + externalSocksPort flows + load/save
  bypass-approval). TorSharedPreferences implements it via forwarding properties.
- Injected ioDispatcher (default Dispatchers.IO) and nowMs clock (default
  System::currentTimeMillis) so tests drive the 45s watchdog + 5-min cooldown
  in milliseconds of virtual time.
- Tests cover: persisted-approval load, torType-change bypass clear,
  approveBypassForOneHour, onNetworkChange (clear + reset + cooldown prime),
  watchdog gentle-reset before first Active, watchdog full-reset after Active,
  watchdog cancellation on Active, cooldown blocks within window + permits
  outside, status routing for OFF/EXTERNAL/INTERNAL, sessionBypass forcing Off,
  activePortOrNull mirroring.
- Uses UnconfinedTestDispatcher inside runTest — flowOn(ioDispatcher) +
  WhileSubscribed cross-dispatcher channel needs eager dispatch for
  MutableStateFlow.value updates to propagate through advanceUntilIdle.

Tier 3 — TorBootstrapInstrumentedTest scaffold (@LargeTest, @Ignore by default):
- Cold-start bootstrap: TorService.start → first { Active } within 120s.
- HTTPS round-trip: OkHttp via SOCKS to check.torproject.org, asserts IsTor:true.
  This is the regression net for the rustls CryptoProvider install after the
  Arti bump and for the destroy/handler abort race in the Rust shim.
- reset → re-start: verifies the state-file-lock is released so the second
  TorService.start can re-create the TorClient cleanly.
- KDoc documents how to enable + run on a real device (the test needs Tor
  network egress + 60–120s of wall-clock per case, hence default-Ignored).

No production behavior changes — only injection seams + interfaces.
2026-05-26 20:31:32 +00:00
Claude 612e05fa62 feat(amethyst): zapUser supports onchain (NIP-BC) rail
Adds a `chain` parameter to zapUser so Gemini can route the zap over
Lightning (default) or onchain Bitcoin (NIP-BC kind:8333).

  * chain="lightning" (or null, "ln") — existing Lightning flow:
    build kind:9734 → fetch BOLT11 → NWC auto-pay if configured →
    return invoice + nwc fields.
  * chain="onchain" (or "btc", "bitcoin") — new path:
    1. Require the user's Bitcoin chain backend to be configured
       in Amethyst Settings → Bitcoin. Throw NotSupported with a
       pointer to the settings screen if absent.
    2. Validate feeRateSatPerVByte (0.1 ≤ rate ≤ 1000).
    3. Call Account.sendOnchainZap, which uses the existing
       OnchainZapSender pipeline: build P2TR-paying tx, sign,
       broadcast, publish kind:8333 receipt.
    4. Return ZapResult with onchainTxid + feeSats + changeSats +
       receiptEventId on success, or onchainError + onchainStage on
       failure. When the failure stage is "publishing" the tx is
       already on-chain — we still surface broadcastTxid so the user
       can verify on a block explorer.

ZapResult gains a `chain` discriminator field plus six onchain*
fields. Lightning zaps populate the existing fields and null out
the onchain ones; onchain zaps do the inverse. The kdoc on ZapResult
explains the split.

Trigger phrases in the verb kdoc now include "send N sats onchain to
[user]" / "send Alice N sats via Bitcoin" so Gemini's matcher picks
up the onchain intent specifically.

New parameters:
  * chain: String? = null — "lightning" | "onchain" (case-insensitive)
  * feeRateSatPerVByte: Double = 5.0 — fee rate for onchain rail,
    ignored for Lightning. 5 sat/vB targets fast confirmation under
    typical mempool conditions without being aggressive.

zapEvent stays Lightning-only for now — onchain event zaps with
NIP-57 splits need a different pipeline (sendOnchainZapWithSplits)
and the result shape would be quite different. Deferred to a
follow-up if there's demand.

app_metadata.xml updated so the LLM picker pitches the dual-rail
capability to users.
2026-05-26 20:31:29 +00:00
Claude 9a2adf091d chore(tor): bump Arti to v2.3.0
Wins: reduced GeoIP memory usage (moved off heap), CircuitClosed→NotConnected
error change (affects our handler error paths), DATA-cells-on-closed-streams
fix, and a flow-control sidechannel mitigation bug fix. Nothing here directly
addresses the stuck-Tor recovery work in the prior commits, but it's a clean
overdue bump while we're in this code.

Wrapper changes required by the bump:
- arti-client + tor-rtcompat: 0.41 → 0.42 to match the new crate versions
  shipped with arti-v2.3.0.
- arti-v2.3.0's tor-rtcompat no longer installs a rustls CryptoProvider
  implicitly (changelog: "if the application fails to install a rustls
  CryptoProvider, tor-rtcompat no longer installs one itself"). Add a direct
  `rustls = "0.23"` dep with the `ring` feature and `install_default()` it
  inside INIT_ONCE before runtime creation — otherwise create_bootstrapped
  panics on the first TLS handshake. Keeping `ring` (same as 2.2.0
  effectively used) rather than 2.3.0's new default `aws-lc-rs`, which is
  heavier on Android and has known build.rs pain on aarch64-linux-android.

Heads-up for the next bump: arti-v2.4.0 will explicitly wrap TorClient in
Arc rather than implicitly having Arc-like semantics. We already wrap
explicitly so the migration is a no-op aside from potential Arc<Arc<...>>
cleanup.

Rebuilds: libarti_android.so for arm64-v8a + x86_64.
2026-05-26 20:06:15 +00:00
Claude c3ddd4e7be fix(tor): audit fixes — first-bootstrap grace + tighten destroy() race
Audit of db378a1 surfaced three issues; this commit addresses them.

1) First-bootstrap self-heal storm (TorManager). On a fresh install with a
   slow network the legitimate first bootstrap takes 30–60s. The 45s
   stuck-Connecting watchdog used to fire resetWithCleanState, wiping an
   empty state dir and adding a full bootstrap cycle of delay for no gain.
   Now: track hasEverBootstrapped (flipped when status reaches Active);
   pre-first-bootstrap self-heals use the gentler reset (drop client only,
   keep state), post-first-bootstrap use resetWithCleanState. Wiping stale
   on-disk guards only matters once we know Arti can actually work.

2) Rust destroy() race (lib.rs). The accept loop in startSocksProxy has no
   .await between accept() returning and HANDLER_TASKS.push(h), so an
   abort() alone is racy — a new handler can be spawned and pushed AFTER
   our drain runs, which then holds an Arc<TorClient> past destroy() and
   keeps the state file lock alive. Now: after abort(), await the SOCKS
   JoinHandle with a 1s timeout so the listener fully terminates before
   we drain HANDLER_TASKS. No new handlers can be added once the listener
   is gone.

3) TOKIO_RUNTIME mutex held during block_on(sleep). The previous
   `if let Some(rt) = TOKIO_RUNTIME.lock().unwrap().as_ref()` kept the
   mutex held for the full sleep duration, blocking any other JNI caller
   that needs the runtime. Now: clone the runtime Handle and release the
   mutex immediately. Same fix applied to stopSocksProxy.

Rebuilds: libarti_android.so for arm64-v8a + x86_64.
2026-05-26 19:37:36 +00:00
Claude db378a105c feat(tor): self-heal — drop & rebuild Arti on network change and stuck Connecting
When Arti's in-memory TorClient gets into a broken state (bad guards from a
previous network, dead circuits, expired consensus held in memory), nothing
short of a process restart used to recover it: the JNI exposed initialize /
startSocksProxy / stopSocksProxy but no way to drop the TorClient, and the
Kotlin side gated initialize behind a one-shot AtomicBoolean. force-stop
preserved the on-disk arti/state/, toggle-off-then-on only re-bound the SOCKS
listener on the same broken client, and wiping app data was the only way out.

Rust side
- New JNI Java_..._ArtiNative_destroy: aborts the SOCKS listener task, aborts
  all in-flight per-connection handlers (each holds an Arc<TorClient> clone
  that would otherwise pin the state file lock), waits 500ms, drops the static
  ARTI_CLIENT. Next initialize() call creates a fresh client and re-bootstraps.
- Track handler JoinHandles in HANDLER_TASKS so destroy can abort them; cull
  finished ones on each accept to keep the Vec bounded.

Kotlin side
- TorService.reset() / resetWithCleanState() — drop the native client, flip
  initialized=false. The second variant also wipes arti/state/ on disk to
  rebuild guard selection from scratch.
- TorManager.resetEpoch StateFlow is now part of the status combine; bumping
  it re-fires the INTERNAL branch which calls service.start() and runs full
  Arti re-init.
- onNetworkChange (wired from ConnectivityManager.networkId distinctUntilChanged)
  now calls service.reset() + clears the persisted bypass approval + bumps the
  epoch. Replaces the previous clearSessionBypass() which only touched the
  in-memory bypass half.
- Self-heal watchdog: when status sits at Connecting for >45s (before the 60s
  connectionFailure dialog), calls resetWithCleanState. Rate-limited to one
  per 5 minutes so a permanently broken network doesn't loop us. onNetworkChange
  primes lastSelfHealAtMs so a slow legitimate post-network-change bootstrap
  doesn't get a second reset on top of itself.

Rebuilds: libarti_android.so for arm64-v8a + x86_64 (NDK 27, 16KB-page aligned).
2026-05-26 19:09:54 +00:00