<feed xmlns='http://www.w3.org/2005/Atom'>
<title>coinosv3.git/src/nostr.js, branch fdroid-v1.7</title>
<subtitle>coinos v3 — bitcoin, Ark, Lightning and nostr wallet (web + Android)</subtitle>
<id>https://git.coinos.io/coinosv3.git/atom?h=fdroid-v1.7</id>
<link rel='self' href='https://git.coinos.io/coinosv3.git/atom?h=fdroid-v1.7'/>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/'/>
<updated>2026-09-15T18:56:52Z</updated>
<entry>
<title>Chat keeps its relay subscriptions alive while the app is on screen.</title>
<updated>2026-09-15T18:56:52Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-15T18:56:52Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=0ebef05d3850d312ec8098584525b22ec1b9a0de'/>
<id>urn:sha1:0ebef05d3850d312ec8098584525b22ec1b9a0de</id>
<content type='text'>
New messages rang in Vector but sat unseen in v3 until a reload. The
resume hook only rebuilds subscriptions after an absence it was told
about; a socket that errors while the page stays visible (a wifi hop, a
laptop lid, a phone freezing the tab without the away timer noticing,
another tab clearing that timer first) is dropped from the nostr-tools
pool with every subscription it carried, and nothing dials it again.

A watchdog now runs whenever the app is on screen: on focus, on becoming
visible, and every ten seconds between. It asks the pool whether each
relay the inbox and the rooms listen on still holds a live socket (a dial
hung past 20s counts as dead, and a relay that closed a subscription on us
counts too) and rebuilds the lot when one is missing, dropping a stuck
relay first so the rebuild dials afresh. Rebuilds retry on a growing
backoff while a relay keeps refusing. tools/chat-watchdog-test.js errors
every socket under an open room and checks the recovery.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
</content>
</entry>
<entry>
<title>The feed was showing half the posts it should.</title>
<updated>2026-09-13T05:15:10Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-13T05:15:10Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=d57962cb896b360295ec107afd50d2ce8701c401'/>
<id>urn:sha1:d57962cb896b360295ec107afd50d2ce8701c401</id>
<content type='text'>
Compared against a wider net over the same six hours and the same follow
list — 862 people, read off this wallet:

  before:  165 notes from 38 people
  after:   327 notes from 86 people, and nothing the wider net still finds

Four things were losing posts, in order of how much:

THE AUTHOR CAP. feedAuthors() stopped at 500. On an 862-follow list that
silently dropped 362 people, and 164 of the 166 posts the wider net found
were theirs. Gone: it takes everyone now, up to a couple of thousand, with
each REQ carrying at most 400 authors so no single filter is absurd.

THE DEFAULTS. relay.damus.io is the second-biggest relay this follow list
writes to — 249 of them — and it wasn't in our set. 232 follows publish no
relay list at all and lean entirely on those defaults, so they were being
asked for on two relays instead of three.

THE SOCKET CAP. Ten relays covered all but 66 follows; twenty-five covered
all but 26. It's 24 now — Amethyst runs near a hundred, and this is the same
idea kept to a number a phone can hold.

MALFORMED RELAY LISTS. One list in these follows crams three URLs into a
single r tag, space-separated. It passed our "starts with wss://" check,
reached the pool, threw Invalid URL — and every author covered by that relay
quietly returned nothing. Each URL has to parse now.

And a quote-post is no longer mistaken for a reply. NIP-10 marks a reply's e
tags 'root' or 'reply'; a quote's are 'mention', and NIP-18 quotes carry a q
tag. We were hiding every note with an e tag, which included the quote-posts
that now render as cards.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>Answer a post: like it, boost it, quote it, or stop seeing its author.</title>
<updated>2026-09-12T15:28:02Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-12T15:28:02Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=f34b5a155e7cb63e15eff7e7ba9559292b31c5e1'/>
<id>urn:sha1:f34b5a155e7cb63e15eff7e7ba9559292b31c5e1</id>
<content type='text'>
Under every post now: a heart with its count, a boost with its count, and a
quote. Behind an ellipsis beside reply and zap: quote, boost, copy the note's
reference, mute the author, or block them. Likes and boosts come back in the
SAME relay request the zap tallies already make — one round trip answers all
three — and each action paints before any relay does, the way the zap chip
learned to.

A like is kind 7 and a boost kind 6, published to the author's own write
relays as well as ours, so the person being answered actually sees it. A
quote opens the composer with a nostr:nevent reference already in the text
and tags the post on publish, so other clients render the embed. Taking a
like back is the only withdrawal nostr has — a deletion request naming the
reaction.

Mute is the NIP-51 list (kind 10000): their posts leave the feed at once and
the list is published, so it follows you to other clients. Block is the same
list plus an unfollow, which is why it says so.

And a bug the mute found, which the follow list had too: the optimistic paint
was being read back as fact. Both toggles paint the answer immediately and
then publish onto the newest list the relays will give them — but when the
relays had nothing newer, "the newest list" WAS the optimistic paint, so the
code saw the follow already present and published a list without it. Every
follow was a no-op that looked like it worked. The base is now the relays'
copy only when it's genuinely newer, and otherwise what we held before the
paint.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>Each account in the switcher wears its own face, and its own renewal warning.</title>
<updated>2026-09-11T04:05:58Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-11T04:05:58Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=3cda220c2432181b5f2dfc0c47e28c3e3cb54f18'/>
<id>urn:sha1:3cda220c2432181b5f2dfc0c47e28c3e3cb54f18</id>
<content type='text'>
Two accounts could show the same name and avatar: the identity list read a
key stamped onto the account record, and the stamp was taken during the gap
in a wallet switch where the new account is already loaded and saved but the
features haven't re-inited — so the PREVIOUS account's signer was still
answering "who are you". The live signer now re-checks on every read that it
still belongs to the open wallet, not just at init(); and the switcher reads
each account's own persisted login (or its seed's nostr key) instead of the
stamped mirror, so a record stamped wrong before this still lists right.

The Spending renewal warning belonged to the wallet that computed it and
stayed on screen after switching — the old wallet's amount and deadline over
the new wallet's balance, with a 30-minute throttle keeping it there. It now
resets with the ark manager, and the next connect re-decides.

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
</content>
</entry>
<entry>
<title>Durable sync outbox: fix the fresh-device phantom "Sent" that showed a zero balance (authored by codex).</title>
<updated>2026-09-07T06:52:21Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-07T06:52:21Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=989e29302e479277fb18ee105874c9a1eeb00be5'/>
<id>urn:sha1:989e29302e479277fb18ee105874c9a1eeb00be5</id>
<content type='text'>
Root cause of the "signed in and my balance is 0 with a huge Sent I never made": a device's snapshot carrying its change/new coins never reached the relay. The publish was a setTimeout debounce that got canceled on page-hide / logout / feature stop, so coins created by another session were never synced; a fresh device then loaded only the old (now-spent) coins, and reconcile dressed the spent inputs as a definitive "Sent" row and zeroed the balance. No funds ever moved — verified on the ASP that no such payment existed and the change coin was still spendable.

Fix (codex):
- src/sync-outbox.js (new): a durable outbox. Snapshots are signed synchronously at save time under the current identity (no cross-account mixing) and persisted already-encrypted to localStorage (no keys/plaintext in storage). Delivery retries with backoff and wakes on online/pageshow/visibilitychange/storage, surviving suspension, logout and relay outages. The last copy doubles as a local recovery source if an account cache is cleared.
- sync.js: replace the cancel-on-stop debounced publish with the outbox; fetchAllStates also merges the outbox's own (possibly-unpublished) events so a rejected publish is still recoverable locally; forgetAll clears it.
- nostr.js: factor out stateEvent() (synchronous signing) and decodeStateEvents(), shared by fetch and the outbox.
- ark.js: adoptArkSnapshot() applies a synced snapshot to a LIVE connected manager without restarting payment ceremonies (a snapshot arriving while connected previously stayed invisible until reload); the pre-handshake hold-key merges instead of overwrites; and the reconcile row is reworded from a red "Sent -N" to a neutral "History incomplete / Payment details not synced" (↻) with an explainer, since a spent input proves neither an amount nor a date.
- app.js: clearAll also clears the outbox (forgetAll).

Tests (codex): sync-outbox-test, sync-browser-test (real Chrome: relay rejects a snapshot, browser closes, reopens and retries; a second browser recovers the balance + history without reload), sync-split-test, plus build and morph smoke — all green.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
</content>
</entry>
<entry>
<title>Move nip44 decryption off the main thread — the remaining boot secp256k1 after verification moved off (chunk-60dfb4nt in profiles). The verify worker grows two ops: 'openWraps' opens community (Concord) wraps given the symmetric stream conversation key — nip44 decrypt + the seal's schnorr verify + hash checks per wrap, no secret crossing the thread boundary since stream keys are group material; 'unwrapDMs' runs the full NIP-17 unwrap (two ECDH ops + seal verify) when the wallet holds the raw identity key. Remote signers can't export their key, so their DM path stays on the main thread, exactly as before. The time-sliced main-thread queues survive as fallbacks — a dead or stalled worker only changes WHERE the crypto runs (workerCrypto resolves null and callers re-enqueue), never whether chat catches up. Bonus: subChannel derives its stream key once per channel instead of once per wrap (groupKey does hkdf + ECDH per call — real curve work on a 200-wrap backfill)</title>
<updated>2026-09-04T15:24:04Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-04T15:23:50Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=7bdc0fb00f8f77075a449732971f4689d892e921'/>
<id>urn:sha1:7bdc0fb00f8f77075a449732971f4689d892e921</id>
<content type='text'>
Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
</content>
</entry>
<entry>
<title>Nostr signature verification moves off the main thread: the pool's synchronous verifyEvent (a schnorr check per event, ~560ms on a relay backlog — the profiled carousel-stutter cause) is switched off, and every event is now gated through a Web Worker before any caller sees it. Nothing unverified is ever trusted — the worker runs the same nostr-tools verifyEvent, just on another thread, with a synchronous fallback if the worker is unavailable (standalone file://) or stalls. Bundled to dist/verify-worker.js, precached in the SW</title>
<updated>2026-09-04T05:55:03Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-04T05:55:03Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=618a84ab96f1865f6112d696077390ba184dd9b3'/>
<id>urn:sha1:618a84ab96f1865f6112d696077390ba184dd9b3</id>
<content type='text'>
Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
</content>
</entry>
<entry>
<title>nevent and nprofile references come alive: nprofile mentions render as clickable @names beside their npub siblings, and note/nevent references become 'view note' links that fetch the event by id (relay hints first) and open its thread right in the app — the dim truncated stubs survive only for what genuinely can't resolve</title>
<updated>2026-09-03T02:03:19Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-03T02:03:19Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=a2069d6ad1423b1788428b93b6bbb4531c551572'/>
<id>urn:sha1:a2069d6ad1423b1788428b93b6bbb4531c551572</id>
<content type='text'>
Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
</content>
</entry>
<entry>
<title>Delivery moves at the speed of the best relay, not the worst: publish resolves on the first acceptance, DMs stop idling on the peer's inbox lookup, and the dimming departs — a sent message simply appears</title>
<updated>2026-08-18T19:16:36Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-18T19:16:36Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=1d21e3b9546496a4eccf8ec9ec45934999e07fd8'/>
<id>urn:sha1:1d21e3b9546496a4eccf8ec9ec45934999e07fd8</id>
<content type='text'>
Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_012u4oe1KPx9faCsDBLfZYqi
</content>
</entry>
<entry>
<title>Wallets go flat: every Spending and Savings is a top-level wallet in the switcher, named for its purpose, renamable apart — and a new wallet can carry its own seed or live at the next BIP84 account of one you already back up</title>
<updated>2026-08-15T21:32:28Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-15T21:32:28Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinosv3.git/commit/?id=643046319fa9a393374fc2348381d9505fe143eb'/>
<id>urn:sha1:643046319fa9a393374fc2348381d9505fe143eb</id>
<content type='text'>
Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv
</content>
</entry>
</feed>
