| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
It was rendering everything it held — a hundred-odd notes to show the few you
read. It now paints twenty, and reaching the bottom shows twenty more; only
when the window catches up with what's cached does it go back to the relays
for older posts. Posts arriving live at the top widen the window rather than
pushing off the one you'd scrolled to.
And the posts are the page now, not a card sitting inside one: they step out
through the chat page's padding to the container's own edge, leaving one
continuous list with hairlines between the posts instead of two sets of card
walls.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A payment is worth what it was worth WHEN IT HAPPENED, and that number is
gone by the time you look at it unless someone wrote it down. So every price
the app fetches is kept as a sample — one an hour, a few months back — and a
payment's fiat value is read off the sample nearest its own timestamp. The
history rows and both detail pages (on-chain and Spending) pass the time the
money moved; balances and forms use the live price, as they should.
A payment older than any sample we hold is priced at today's number and says
so with a '~', because that is a different claim and shouldn't look like the
same one. The detail page spells it out: "Worth then $X · $Y now".
Fiat is a third position on the unit toggle rather than a second number
beside the first: sats → BTC → your money → sats. One figure, and which
figure is your choice. The ring skips fiat entirely until a price has been
fetched — a third position showing blanks is worse than two that work. Typing
an amount with the unit on USD means dollars, priced now.
The rates are coinos' own endpoint, a map of every currency it knows, so
there's no third party watching what the wallet asks about; the currency
picker under Settings → Network lists all of them. Changing it starts the
recorded history again rather than relabelling samples taken in another
currency.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Our set is three relays — relay.coinos.io, nos.lol, relay.primal.net — plus
whatever the user's own NIP-65 names. Asking those for everyone's posts only
finds the people who happen to use them; anyone else looks like they stopped
posting.
So the feed now reads NIP-65: relay lists are fetched in batches of a hundred,
kept for a week, and the authors covered greedily — the relay carrying the
most people not yet covered, repeated up to ten sockets. A feed of hundreds
is a handful of connections that between them reach everyone, rather than one
each.
Our relays are still asked about everyone, not just the people with no list.
Measured across eight busy accounts, each source holds posts the other misses:
reading the author's own relays surfaced 7 to 40 notes ours had never seen
(jb55 +40, verbiricha +25, pablo +23), and ours held plenty theirs didn't —
fiatjaf's own three relays answered 42 where ours answered 67. The feed wants
the union, so the outbox adds reach rather than replacing it. On the test set
that took the feed from 83 posts to 118.
Profile pages get it for free: relaysOf now reads the batch's cache first, so
opening someone you follow costs no extra round trip.
The test records every socket the page opens and checks the plan by what it
actually dialled.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
FOLLOWING. A profile page gains Follow / Following beside DM and Pay. The
contact list is the kind-3 event, kept locally so the button is right in the
first frame and refreshed from relays behind it. Publishing one is delicate:
a contact list is a shared document — other clients keep relay hints in its
content and petnames in its tags — so a follow re-fetches the newest list
first and adds or removes exactly one p tag, preserving everything else. The
button flips immediately and flips back if the publish fails. The service
worker's friend filter reads the same list, so following someone also means
their DMs stop being treated as a stranger's.
THE FEED. Posts by the people you follow, newest first, at the top of the
messages home. Replies are left out: a reply belongs to its thread, and a
timeline of half-conversations reads like eavesdropping. The last screenful
is kept locally so it opens with posts in it rather than a spinner, new posts
arrive over a live subscription while you're looking at it, and older ones
stream in as you reach the bottom — the same shape as the profile pages. A
wallet that follows nobody is told so and offered the people search.
MEDIA. The composer (now shared by the feed and the profile page) takes a
picture or a video: it uploads when you pick it and appends the URL to the
text, which is how every nostr client reads media, with a NIP-92 imeta tag
published beside it for the ones that would rather be told than guess. Posts
already render images and video inline, so they show up where they land.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Pinguimdarelva's avatar still flashed, and the reason was one I'd have kept
guessing at: from the live browser, `new Image()` with crossOrigin set FAILS
for that picture, while `fetch(url, {mode:'cors', cache:'reload'})` for the
very same URL returns 200. The host allows us perfectly well.
The picture sits behind a 302, and the avatar is painted as an ordinary
background-image — so the bytes are already in the HTTP cache under the
redirect's target as a no-CORS entry, and the crossOrigin <img> reuses that
entry and fails the CORS check. The image loads, the CORS copy "errors", and
the face gets written off.
So the thumbnailer fetches the bytes itself and draws from a blob: no
crossOrigin element, no cache collision, and an untainted canvas without
depending on the element's CORS bookkeeping. With no taint left to detect,
failures no longer split into hard and soft — each one just backs off, from
six hours up to a fortnight, so a host that truly won't have us is asked
twice a month instead of every boot.
The test now serves a picture behind a redirect and loads it plainly before
thumbnailing it, which is the shape that broke; it fails on the old code.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Pinguimdarelva's picture is 52KB on a CDN that answers in 700ms one minute
and times out the next — I measured both from this machine. The thumbnail
attempt hit one of the bad minutes, and `thumbFail` was written as if the
host had refused us: never retried, so that avatar went back to fetching the
original on every boot.
Only a tainted canvas is a real refusal (no CORS header, nothing to be done),
and that is remembered for good. Everything else — a load error, or a host
that won't answer inside twenty seconds — is stamped with the time and tried
again in six hours. The timeout also frees the slot: three thumbnails run at
a time, and a hanging image used to hold one of them indefinitely.
And zap tallies are remembered between sessions. They were asked of the
relays on every boot, which takes seconds, so a room full of zapped messages
painted bare and filled in afterwards. The totals (never the receipts) are
kept locally and painted straight away; the moment the relays answer for a
message, their word replaces it — so a tally that changed, or a receipt that
was deleted, still settles correctly. Opening a DM thread doesn't forget what
the chat had learned, and the store is capped at 400.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The avatar delay left on the messages page wasn't the image at all. A person
with no cached profile got an empty circle until a relay answered — and the
one that stayed blank longest (npub1k6f89cg, in the live cache) is never
cached at all: persistProfile deliberately refuses to write a row with no
name and no picture, so every boot re-asked the relays and every boot sat
blank for seconds. When it finally arrived it was a punk, which is why it
looked like slow art rather than a slow lookup.
The punk is derived from the pubkey alone, so it can be drawn in the first
frame. It's already the right answer for everyone with no picture, and for a
coinos user who kept the default it IS their published picture — same
formula, same file. A real photo replaces it when it lands. A name lookup in
flight still gets the quiet circle: a picture is expected there, and punk art
must not flash in front of it.
Also: the punk rewrite now recognises the bare relative path a few older
profiles carry, not just the absolute URL.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The art is 240x240 (20KB each, 1.5MB for the set) because that's what a punk
published as someone's nostr picture should be — but the circles the app
draws are 18 to 64px, so a new wallet with nothing cached was fetching eight
times the pixels it could show, one request per face.
punks-sm carries a 128px copy of each: 4KB instead of 20KB, indistinguishable
at 64px. Small circles and the wizard's picker tiles load those; only the 64px
profile avatar and the value the wizard publishes stay full-size.
The bigger win is that a punk picture is our own art wherever it turns up.
Everyone who keeps the default publishes v3.coinos.io/punks/N.webp as their
nostr picture, so those faces now paint straight off disk at the size being
drawn — no request, no thumbnail to make, and no broken circle when the
legacy coinos.io copies 502 (which they do for a third of the set).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A profile picture is whatever its owner uploaded, and that is very often the
full-size original. Measured against the faces this wallet had cached: one
avatar is a 4,988,208-byte JPEG, another 3,901,966 — megabytes fetched and
decoded to paint a 30px circle. That is the second of blank circles on the
messages page after a refresh, and it repeats every time the upstream cache
header expires.
So the first time we see a picture we downscale it once (96px, webp) and keep
the thumbnail beside the profile in localStorage, which is read synchronously
at boot. Small avatars then paint from it alone and never ask the network for
the original again; the 64px profile avatar layers them, so the thumbnail
shows instantly while the full-size original loads over the top.
Reading the pixels needs CORS — most avatar hosts allow it (6 of the 9 here),
and the ones that don't are remembered as failures rather than retried every
boot. A refresh of the profile carries the thumbnail across when the picture
hasn't changed. Thumbnails live on a 180k budget, oldest face first, and Data
Saver opts out of making them at all.
tools/avatar-thumb-test.js watches a wallet meet a large cross-origin picture
and checks the next boot fetches no picture at all.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Boot decides whether a session restores, but plenty renders before it gets
there — the deferred feature chunks land and repaint, an emitter ticks — and
a render that early has no idea whether anyone is signed in. It painted the
welcome screen anyway, so a signed-in wallet flashed the sign-in page for the
~75ms before boot restored it. Waiting for the ark store widened the gap; the
race was already there.
The signed-out screen is now the one screen boot won't guess at: until the
session question is answered, the frame holds the boot shell (the brand row
it's already showing). Every other screen is a deliberate choice — a gift
claim, a locked vault, a chat — and still paints immediately. A release timer
makes sure a boot path that returns early can't hold it back for good.
tools/boot-signin-flash-test.js widens the race and checks every frame of
four reloads; before this it caught the flash on every one.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The Spending balance and its history are read from the ark store, which is
IndexedDB: synchronous to read, asynchronous to OPEN. Boot didn't wait for
it, so every refresh painted a Spending wallet with nothing in it — no
balance, no history — and filled it in a beat later. On a phone that second
paint arrived as a change rather than an arrival: the balance counted itself
up from 0 in green, wearing the animation a received payment gets.
Boot now waits for the store (capped at 800ms — a store that won't open must
not hold the app hostage), and the shell stays up a moment longer instead of
a wrong balance going up. If the store does land past the cap, initArk
retries on it — so a Spending wallet whose state arrived late also connects,
which it previously didn't until something else happened to kick it — and
that catch-up paint forgets the last amounts first, so it reads as state
arriving rather than money.
Also: the zap tally chip loses its underline. In chat the chip is a reaction
chip too, and .chat-react.on still rings it; on a post row the underline just
read as an artifact.
tools/boot-balance-test.js pins the ordering, with IndexedDB slowed so it
can't pass by luck.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Feature screens (a profile, a chat thread, a search) are drawn ahead of the
wallet screen — and the padlock's "Protect this device?" card was drawn BY
the wallet screen. So tapping the padlock from a profile did nothing you
could see: the card was waiting behind the page, and only appeared once you
navigated back to the wallet. The ask now outranks feature screens (with
onboarding still ahead of it), and declining leaves you exactly where you
tapped, because nothing else moves.
A lock that goes through does move: it drops the profile/chat/search the
padlock was tapped from, so you land on the wallet wearing a closed padlock
instead of reading a profile page with only a toast to say what happened.
Logging out drops it too — a page belonging to the locked session had no
business outliving it, or greeting whoever unlocks next.
tools/padlock-ask-test.js covers the tap and the decline.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A 5,000-sat send built on 2026-09-06 that the network never took sat in the
history as a pending withdrawal for four days on every device. The local
scan prunes such rows (explorer 404 / absent from address history), but
the boot-time cross-device history union re-adopted it from a stale device
slot's core snapshot, and the device then republished it — a phantom that
no explorer knows, immortal across devices.
The union now adopts a remote PENDING row only while it is under a day old
(it has to prove itself to the local scan); confirmed rows still union as
before, and the device's own rows are untouched.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
localStorage held a deep wallet's ark state as ~700KB of JSON (UTF-16 on
disk), parsed on every boot and stringified on every save, and the wallet
cache blob carried a second ~600KB copy. src/ark/store.js keeps the state
in IndexedDB (db coinos-ark) with each coin's signed bytes as a Uint8Array,
fronted by an in-memory map so loadArkState/saveArkState stay synchronous:
reads return clones, writes coalesce (200ms) and flush immediately when the
page hides. Existing localStorage copies migrate on first open (newest _rev
wins) and are removed only after the IndexedDB write completed; no
IndexedDB means localStorage exactly as before. Records keep their
btc-wallet-cache:<id>:ark… keys, and a wallet wipe clears both stores via
the new per-feature wipeCache hook.
The ark cache extension is now syncOnly: it still rides the sync snapshot
(and the Blossom blob) but the local wallet cache no longer embeds it —
measured on a live wallet the cache blob went from 655KB to 10KB.
Verified in the live tab: all wallets migrated, bytes raw on disk, balance
and coin set unchanged, localStorage ark keys gone. tools/ark-store-test.js
covers migration by _rev, clone-on-read, write-behind, wipe and fallback.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
read, hex still on the wire.
The live coins' signed bytes are most of a deep wallet's state — 560KB of
hex, which localStorage then holds as UTF-16. vtxoBytesToStr/FromStr in
proto.js persist base64 and read either form, _addVtxo writes base64, and
pruneArkState converts hex to base64 on every save, so an existing state
migrates itself the first time it saves. Measured on a live deep wallet:
904KB → 665KB (-26%), all 41 coins decoding identically.
The sync wire (slimArkForSync) keeps sending hex for now: a device still
on an older build would otherwise fail to decode every coin merged from a
newer one. Flip it once pre-2026-09-10 builds are gone.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
actions shed hex.
pruneArkState runs on every manager save: a spent coin loses its signed
bytes once its spend is 30 days old and no in-flight action names it (a
spent coin can never take the exit those bytes exist for), and an action
done more than 7 days ago loses the output-vtxo hex it minted (the change /
HTLC coins live in `vtxos` in their own right). Ids, amounts, inputIds and
preimages stay for history rows and spend dedupe; failed ln-pays keep their
HTLC bytes for the refund. Selective and idempotent; unit-tested.
Measured on a live deep wallet the immediate saving is small (every recent
spend is inside the window) — what remains is the live coins' own chains,
which only a renewal shrinks. The change is a growth cap, not a diet.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A wallet with deep Ark coins had a 633KB ark sync domain against the relay's
40KB budget: the publish silently failed forever, the slot stayed stale,
and every boot retried. Relays can't carry it (64KB event cap) and neither
can NIP-44 (64KB plaintext), so a domain over budget now goes to Blossom
(nostr blob storage, BUD-01/02/06/11/12): sealed into an envelope
("CSB1" + xchacha20poly1305 under an HKDF of the wallet's NIP-44 self-key),
uploaded to the configured servers, and the kind-30078 event carries only
{ blob: { sha256, size, servers } }. Readers resolve pointers from their own
outbox copy first, else from the pointer's servers plus the configured
list, hash-verified. Small domains are unchanged.
The outbox uploads before it publishes (upload/delete authorizations are
kind-24242 events signed at enqueue time, so delivery after logout still
holds no key), keeps a local copy for recovery, and deletes superseded
blobs once the replacement is acknowledged.
Servers: Settings → Nostr → Sync storage (any Blossom server; a typed list
is kept), else the user's BUD-03 list (kind 10063) with the coinos server
as mirror, else blossom.coinos.io.
blossom/server.js is that default: a Bun Blossom server that stores ONLY
these envelopes (magic check → 415 for anything else), serves every blob as
an octet-stream attachment with nosniff, requires signed auth for
upload/list/delete, and enforces per-blob size, per-key quota (oldest
evicted), rate limits, a disk cap and unused-blob expiry — it cannot serve
as a file host. tools/sync-blob-test.js spawns it and covers the policy,
the outbox flow and a fresh device resolving a pointer.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
|
| |
cdp-profile.js samples the page through the remote-debugging port around a
reload / simulated refocus / manual action, and can serve a local dist into
the real tab (service worker bypassed) to profile a fix before it ships.
cdp-callers.js answers "who calls X" from the saved profile.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
| |
_buildGiftPsbt (the v2 ephemeral-key gift builder) subtracted the funding fee — _giftFundFee returns a Number — directly from BigInt coin sums (sum - _giftFundFee(...), sum - gift - _giftFundFee(...)), which throws in JS. Any on-chain (Savings) gift creation hit it: "Split a coin first", "Lock the whole coin", and gift-all. Wrap the fee in BigInt at those sites (new fundFee() helper).
Adds tools/gift-build-test.js — the on-chain gift BUILDER had no coverage, which is how this shipped. It builds a specific-amount gift, a whole-balance gift, and the split carve-out shape (all three fee branches) against a mock wallet and asserts they produce claimable v2 codes without throwing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
balance (authored by codex).
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
client-side, and every invoice the ASP fetches over onion messages is verified here — payment hash read from the raw TLVs, offer terms mirrored byte-for-byte, the issuer's schnorr signature checked against LDK's exact merkle construction (proven against a live CLN-signed fixture) — so no server can hand back an invoice whose preimage it already holds. Paste an lno1 or resolve a name whose record carries one and the existing HTLC pay flow carries it (captaind's Invoice enum takes lni strings natively); a name's owner may now publish their OWN node's offer in their DNS record, retiring the registrar's stand-in — senders fetch invoices straight from their node and no coinos service ever touches the money. The /lnquote fallback prices blinded paths by entry node + advertised payinfo
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
|
| |
|
|
|
|
|
| |
shared chunks alongside, 61KB of gifts/NWC/hats deferred past first paint into slots that keep their hook seniority), the lazy scripts wear IIFE coats so their names can't collide with the page's — which is what silently broke the remote-signer button — nip98 moves out of the login module so minimal builds stop hauling backup relays, and the smoke test learns the front door's new steps while the regtest stack rises from its IBD nap
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
|
| |
|
|
|
|
|
| |
static boot shell baked in — brand row, and the profile frame when the path names one — while the 789KB bundle moves to a content-addressed app-<hash>.js the browser keeps (a repeat visit re-downloads kilobytes, not the app); the nostr-tools barrel import goes subpath, and the NIP-46 signer client ships as its own lazy file fetched on first bunker login
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
|
| |
|
|
|
|
|
| |
stale sibling copies of its connections, the responder pays from the record whose network the invoice names (standing down when none fits), a ghost coin reconciles and retries instead of dying, and error replies wait out a grace period so a sibling's success reaches the client first
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017Fc8SHJRvBBXgMCntjNCri
|
| |
|
|
|
|
|
| |
step, hashed with the device's own randomness so it can only strengthen the phrase — and the forget option now says all data, which is what it takes
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
|
| |
|
|
|
|
|
| |
the full forget, each keeping its own warnings
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
|
| |
|
|
|
|
|
| |
forget this device burns it down — behind the Delete-all warning, which now also sweeps the cached balances and history it always implied
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
|
| |
|
|
|
|
|
| |
Clear all says Delete all — the word for what it does
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
|
| |
|
|
|
|
|
| |
auto-save through it again, setting a real password re-encrypts every saved wallet instead of dropping the absent ones, and the save-first lock prompt finally names the password it wants
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
|
| |
|
|
|
|
|
| |
a password once ever (the guard is finally wired to its comment), the padlock keeps asking because it must, and the suite walks the real UI again
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
|
| |
|
|
|
|
|
| |
open wallet and Not now means exactly that, the logo opens a passwordless vault instead of guarding it, and a seed is born as Savings alone — Spending appears when you actually set it up
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
|
| |
|
|
|
|
|
| |
longer poisons the frame buffer into a 2GB firehose hoard, and exotic txs get their txid computed by hand instead of dropped
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
|
| |
|
|
|
|
|
| |
hover, focus, selection and scroll simply persist — with a smoke test that walks the app and proves node identity survives
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv
|
| |
|
|
|
|
|
| |
anywhere permission is already granted it arms itself — only an explicit Off is honored forever
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014tE4U5nEjGTQy8PwYqamfv
|
| |
|
|
|
|
| |
address to land on becomes a gift only their key can open
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
0.6.0 servers require the new hashlock clauses (OP_SIZE 32 guard) for
Lightning and rounds. New policies/transitions carry the v1 scripts and
type bytes; _v0 variants keep old vtxos reconstructing and exiting.
The retired third-party HTLC policy is dropped (upstream took 0x08).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01M5kFodm9azZJVTQxnkDdKe
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The warning: when the Spending balance minus round fee is under the
330-sat round minimum AND a vtxo enters the renewal window, auto-refresh
can't save the coins — a wallet-screen notice (and a daily local
notification, when permitted) says to receive any amount or spend
before the deadline. New walletNotices feature hook renders under the
balance card.
The amnesty: sub-dust vtxos have no unilateral exit to protect, and
regtest proves captaind keeps cosigning their spends past expiryHeight —
even after the watchman claims the backing on-chain
(tools/ark/expired-dust-test.js). Only hal's own receive validation
stood in the way; it now exempts sub-330 vtxos from the expiry check.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015QwAtBSpKvECqp1rHLHjJk
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Regtest-proven (tools/ark/dust-gift-test.js): the ASP cosigns all-dust
arkoor sends, so a sub-dust bearer vtxo sweeps to its claimer fine, and
dust folds into rounds alongside bigger siblings. The 330-sat gift
floor guarded a failure that can't happen — dropped to 1 sat.
The experiment also caught a live bug: refresh() of a balance smaller
than the round fee computed a negative output amount, and pbWriter's
varint sign-extends negatives into an infinite loop — auto-refresh
would OOM the tab of any wallet too small to renew. varint now throws
on negatives and refresh() refuses cleanly, before marking coins
pending, when balance minus fee is under the server's 330-sat output
minimum.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015QwAtBSpKvECqp1rHLHjJk
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The sign-in screen stacked 'Connect a signer app' directly above a
bunker/nsec field, which reads as two steps rather than two ways in.
A divider says which it is.
The wizard's second step asked whether you have a nostr account, then
sent you to a screen that asks the same thing. Step one now offers both
paths directly — Get started, or sign in with Nostr — and the middle
step is gone, along with its strings. The welcome blurb goes too: the
screen is one decision, and prose above it is something to read before
you can make it.
The footer sinks to the bottom of the window rather than trailing the
content up the page. #app is a full-height column now; the chat card
sizes itself against the viewport, so the layout test covers it at
three sizes.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Locking with no password locks nothing — the wallets sit on the device
under an empty one and reopen with a tap. So the first deliberate lock
now asks whether to set one, explaining plainly what happens if you
don't. 'Not now' is a real answer, recorded, and never asked again: a
wallet that nags is a wallet people stop reading.
The offer rides the deliberate lock only. lock() is also where the idle
timer, the last-wallet-removed path and boot-after-expiry land, and a
password form appearing because you went to lunch is a different app
than the one you left.
Two subtleties worth naming: the offer is set after lock()'s ui reset
block, which clears ui.pw and had been swallowing it; and taking the
offer re-enters lock(), which now finds a password and lands on the
unlock screen instead of the wallet list.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
'Logout' promises a return to a fresh app with nothing of yours left on
it. This closes the open wallet and keeps everything saved, which is why
it read as interchangeable with 'Clear all' — the one that really does
send you back to the beginning.
Renamed via a new key rather than rewording the old one: nineteen locale
tables carry their own word for 'Logout' and t() prefers the locale's
value, so reusing the key would have kept the wrong meaning in every
language but English. The dead key is dropped from those files; they
fall back to English until translated.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Wallets save themselves to this device as soon as you open one, encrypted
under an empty password unless you choose one. Logging out still sent
you to 'Unlock saved wallets — enter your password', which asks for
something that doesn't exist and makes logout look like it did something
drastic. It didn't: logout only drops the keys from memory, and 'Clear
all' is the one that deletes.
With no password, logout now returns to the wallet list — honest, since
nothing is protecting those wallets and a blank prompt protects nothing.
With a password set it prompts exactly as before.
Fixed underneath: mergeVaultList derived the comparison xpub with
accountXpubFor's mainnet default, so on any other network it never
matched the stored one and every unlock re-added the same wallet as a
fresh entry — visible as the same wallet listed twice, once watch-only.
The seed-mismatch checks in 'load recovery phrase' had the same default
and would have rejected the correct seed on testnet.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
It looked like the dot was flickering on and off after a refresh. It
wasn't: measured over the first twelve seconds it appeared once and never
went away, while its entrance animation started seven separate times.
render() rebuilds the whole DOM, so the pop replayed on every repaint.
A dot that means 'someone is waiting' has nothing to animate about, so
the animation is gone rather than gated. tools/unread-dot-test.js counts
animation starts against real appear/disappear transitions, so a future
entrance effect here fails instead of shipping as a flicker.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The message log had a fixed 62vh height, so the card was head + panels +
log + composer tall and the input fell off the bottom of the screen. The
card is now sized to the viewport and the log is the only part that
flexes: opening the invite or member panel shrinks the scroll area
instead of pushing the composer out of reach.
Three header changes, all the same idea — don't show a control for a
choice nobody is making:
'+' becomes 'Add person', because a plus sign doesn't say what it adds.
A lone channel isn't named at all. '#general' next to a composer that
says 'Message #general' points at a choice of one; several channels
become a select, which is what a choice looks like.
The new-channel field hides behind a 'New channel' button. Sitting open
right after you create a community, it reads as something you're
supposed to fill in.
tools/chat-layout-test.js holds the line at three viewport sizes with
both panels open, and covers the header behaviour through actually
adding a second channel.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The app is inlined in the page, so when a new service worker took
control the page reloaded — otherwise it would keep running the old
build. But navigations are network-first: the refresh that triggered
the update had already fetched the new page, so the reload landed a
second or two later as an unasked-for second refresh.
The page and the worker now carry the same build token and the page
asks which build took over, reloading only on a real mismatch. The
token is hashed before substitution, since a page cannot contain a
hash of itself.
tools/sw-reload-test.js pins all three cases against a swappable
server: a first visit doesn't reload itself, a refresh after a deploy
is the only refresh, and a page left sitting open across a deploy is
still reloaded — exactly once. The previous build takes two loads for
one manual refresh; this one takes one.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|