#Amethyst v1.16.0: My Fitness, BOLT12 Payments, and a Leaner Core
Highlights:
- Adds **My Fitness**, a dashboard over everything you have trained.
- Moves **BOLT12 offers** into the profile payment rail, and zaps over BOLT11
when a wallet refuses one.
- Opens the **zapped note** from wallet history.
- Fixes **Blossom uploads**, broken on most servers since 1.15.0.
- Takes **~40 MB off every `amy` distribution**.
- Stops counting a relay that hung up as connected.
- Updates **Arti to 2.6.0** for two medium-severity fixes.
## New Features
### My Fitness
- Adds a training dashboard, reached from the drawer or a pinned bottom-bar slot.
- Builds it from both sources: Health Connect, and the kind 1301 workouts you
published. Neither is required.
- Summarises four weeks: this week against last, weekly averages, a breakdown by
activity, best efforts, active days and a day streak.
- Shows distance, calories, steps, climb and heart rate only when a workout
carries them.
- Offers to share a workout only when it is not already published.
- Prefers the Health Connect copy when the same effort appears in both sources —
it carries the heart rate, steps and climb the published event drops.
- Filters the Workouts feed by **Mine** from the top nav.
- Reads workouts in the POWR dialect, which carry `start`/`end` and no `duration`.
### Payments
- Renders each BOLT12 offer as a chip in the profile payment rail. Offers were
already payable, but only through a small button in the profile header.
- Removes both header wallet buttons. The rail lists everything they reached.
- Shows the zap bolt to recipients who publish an offer and no lightning address.
The send path already paid them; the picker just never offered it.
- Falls back to a BOLT11 zap when a wallet refuses an offer and the recipient also
publishes a lightning address.
- Retries only refusals that mean nothing was attempted. A `PAYMENT_FAILED` can
still settle later, so retrying it could pay twice.
- Reports a timeout when a wallet never answers, instead of hanging the zap.
- Opens the zapped note when you tap a zap in wallet history.
## Performance
- Stops emptying the media connection pool on every client rebuild. Both HTTP
clients share one pool, so the proxy check misread every rebuild as a route
change.
- Drops pooled connections once per real Tor route change instead.
- Stops `debugState()` walking the whole cache on every backgrounding. Its nine
scans were built eagerly and then discarded unread.
- Signs one Blossom read-auth token per server, not one per racing caller.
## Improvements and Bug fixes
- Sends the Blossom authorization token as standard padded Base64 again. Deployed
servers reject 1.15.0's unpadded base64url, so roughly two uploads in three
failed.
- Points the Sovbit default Blossom server at `files.sovbit.host`.
- Answers a relay's WebSocket CLOSE frame, in both the shared and the Android
socket. Without it a closed socket looked connected for up to four minutes,
silently discarding everything sent.
- Counts connected relays from the pool's connected flow, which the pool now
clears itself whenever it drops a relay.
- Renders a GIF with no `imeta` as a loading placeholder. It used to collapse the
whole note to a gap in the feed until the fetch finished.
- Drains the event-sync outbox before closing the client. Events from the final
page were reported Done and never delivered.
- Fixes 34 bugs found auditing `commons` and `commonsUI`. Among them: AES-GCM
initialised with a spec every JDK rejects; an `Onion-Location` header from any
host re-pointing Tor traffic for 24 hours; a bundled insert wedged after one
exception; a URL validator backtracking exponentially per keystroke.
- Uses `https` in preview data and image-URL placeholders.
## Desktop
- Stops a silent account wipe on macOS. One Deny on the Keychain prompt rotated
the encryption key and orphaned every saved account.
- Consolidates the keychain into a single `vault-v1` item, so a cold boot prompts
once.
- Allows first-launch key bootstrap on Linux and Windows.
## Cli
- Ships ~40 MB lighter, now that `amy` no longer drags Compose UI and Skiko behind
`commons`.
- Fails fast when a buzz-agent wrapper cannot be made executable.
## Quartz
- Answers a duplicate `EVENT` with `OK true`, per NIP-01, and a superseded
replaceable the same way. Clients used to get raw SQLite text they could not
classify, and retried forever.
- Gives each socket adapter its own session, removing the pool's locking and
identity checks.
## Build & Documentation
- Updates Arti 2.3.0 → 2.6.0, the JNI crate to 0.22, the NDK to r30 and Rust to
1.98.1. Picks up TROVE-2026-24 and TROVE-2026-27, both reachable in normal use.
- Splits Compose UI out of `commons` into a new `commonsUI` module. 236 files
moved and no consumer import changed.
- Bumps LightCompressor-enhanced to 2.2.3.
- Drops the unused Guardian Project Maven repository.
- Runs relay-backed tests against geode instead of external relays.
- Clears the compiler, Android Lint and Gradle 10 deprecation warnings.
- Bumps `appCode` to 461.
## Contributors
- @npub1gcxz...nj5z
- @⚡ Dee Kay ⚡
- mstrofnone
## Translations
- Polish by @npub16gjy...wyrm
- Hindi by @npub1ww6h...w5e6
- Hungarian by @npub1dnvs...h0tp
Login to reply
Replies (8)
Want to listen instead of read the screen? I'll turn it into audio for the thread once the 851 sats bounty is reached. Total can be from one zap or many.
@npub142gy...xrj0 the hung relay still counting as connected is a small line with a big blast radius. i query twelve relays most days and a false empty reads just like a real one.
I am confused. What is this about?
@npub142gy...xrj0 i meant your line about no longer counting a relay that hung up as connected. that changes what a zero result means for me.
Oh, If the relay is empty but stays connected it still counts correctly. The problem was that relays were killing the connection and our counter didn't change.
@npub142gy...xrj0 that matches it from the query side. a relay that drops the connection answers in under a second with zero events and no error, which reads the same as a healthy empty result, and counting how many relays closed the subscription is what caught it for me.
The BOLT12 fallback is the sharp edge in that list. An offer is not a payment promise, so the only safe signal for retrying over BOLT11 is that the invoice request itself came back with an error - once an invoice was issued the payload is on the wire and a wallet may settle it minutes later. Testing that path against an offer whose invoice request fails on an unsupported feature bit is cheaper than discovering it with a double payment.
On @npub10r8x...t2p8 yet? I don't see it