| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every coin expires, and until now the only thing standing between a
balance and its expiry was maybeAutoRefresh: it renews when the wallet is
opened inside the last 96 blocks, roughly sixteen hours. Bark's own
developers say the same premise fails on mobile — people don't reliably
open a wallet at the right hour, which is why every mobile Ark client
they know of has moved to delegated refreshes and why they are building
server-driven renewal.
captaind already has it, and ark.coinos.io has been carrying the column
unused: SubmitRoundParticipation takes a scheduled_height, and the server
holds that participation out of every round until the height arrives,
then runs it with nobody online — the round tree branch is signed by the
server, which is the whole point of a delegated refresh. The fee is
priced at the height it will run at, so a standing order lands in the
free bracket rather than today's.
So each spendable coin now gets one registered 36 blocks before its
expiry, six hourly rounds of slack. It is deliberately not an action:
the coin stays spendable, the wallet never reads as busy, and spending it
simply kills the order — the server drops a participation whose input is
gone, and _driveScheduled drops ours. The claim comes later, whenever the
wallet next opens: the mailbox breadcrumb we already post wakes it, and
the leaf cosign and forfeit are the ordinary refresh path from there.
Nothing about the input's expiry blocks that claim, which is what makes
this worth having. A wallet that goes quiet for a month comes back to a
coin already renewed and anchored in a fresh tree, instead of one the ASP
has swept and would have to honor out of goodwill.
One cycle per visit is all it can buy — the replacement's id doesn't
exist yet, so its own backstop waits for the next time the app is opened.
tools/ark/scheduled-refresh-test.js covers the five behaviours on
regtest: the server holds the order until its height, runs it while the
wallet is closed, the wallet claims on return, a spend voids the order,
and a claim still works after the input has EXPIRED (the last one mines a
coin's whole lifetime, so it also re-refreshes the funding wallet first —
an expired vtxo can be renewed but not sent).
Riding along: the expired-coin notice that was already sitting in the
working tree, and manager-test's tampered-vtxo check, which had been
reading base64 vtxo bytes as hex since they moved to IndexedDB.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019fMdLypdg5JaU7bdeUFF8v
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A phone freezes a backgrounded tab: timers stop and the relay sockets are
cut, so the feed came back holding whatever it had when you left and only
caught up if you pulled it down by hand. Features now hear about the
return — app.js calls f.resumed(awayMs) on focus — and the feed answers
it by forcing a pass past the thirty-second throttle and rebuilding its
subscriptions, which were talking to sockets that no longer exist. A
blink between apps (under five seconds) is left alone.
That catch-up raises the second problem rather than solving it: dropping
eight posts in at the top moves the words under the thumb of whoever was
reading. So posts that arrive on their own — live, or as a catch-up —
wait when you're scrolled away from the top. A floating pill says how
many, and the tap that lets them in also takes you up to them. Scrolling
back to the top by yourself lets them in too, since up there they cost
nothing. Anything you actually asked for still goes straight in.
Also, on profiles that still show an npub: the batch now falls back to
the relays whose whole job is indexing kind 0 (purplepag.es and friends),
which we never read notes from and so never asked. profile-batch-test's
"handful of asks" bound was a fraction of the row count, which that
fallback broke; the property it means to assert is that the number does
NOT grow with the rows, so it is a constant now.
tools/feed-live-test.js drives a real signed event down a real open
subscription — an unsigned fake is dropped by the signature gate and
would prove nothing — and checks the page does not move, the pill says
"1 new post", tapping it scrolls to the top, and coming back from hidden
re-asks the relays on its own.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A draft is written as plain text but doesn't arrive as plain text: a URL
ending in .jpg becomes a picture, a youtu.be link becomes a player, a
nostr: reference becomes the note it points at, an npub becomes a name.
The only way to find out how yours would land was to send it.
The eye beside the paperclip — on the composer and on a reply — opens a
preview. It runs the draft through noteBody, the very renderer the feed
uses, so this isn't a lookalike of the real thing: it IS the real thing,
in a dashed frame with a PREVIEW label so it can never be mistaken for
the post itself. It follows the draft live while it's open (and only
while it's open — a render per keystroke is not free).
Attaching a picture opens it by itself. Showing you what you attached is
the answer to the question attaching it raises, and it costs no tap.
tools/compose-preview-test.js covers both the composer and a reply: the
picture renders rather than sitting there as a URL, the YouTube link
becomes a player, the preview tracks typing, and it puts itself away.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Four buttons of equal weight, and only the sign-in ones said what they
did. "Get started" said nothing, so the concrete offer on that screen was
Google — which is a convenience wrapped around a seed phrase, not an
alternative to one.
Now the page says what the wallet is before it offers a button, the way
in reads "Create a new wallet" with the cost under it (a minute, twelve
words), and the three sign-ins sit below a line labelled "or, if you
already have an account".
And the second half of the same problem: a wallet that arrives by sign-in
has a recovery phrase its owner has never seen and nobody can reissue.
So the wizard shows it once, right after the first login, while it still
reads as setting up rather than as a warning out of nowhere. "I've
written it down" is the way on; "Later" is a real answer, and the wallet
then keeps one quiet line — not a modal, not dismissible, gone the moment
the phrase is actually seen, here or in settings.
seedSeen records that per account: creating shows the words, importing
means they typed them, revealing in settings counts. Only a derived seed
starts false, which is exactly the case worth asking about.
tools/onboard-backup-test.js covers both halves, including that "Later"
leaves a reminder that survives a reload and leads somewhere.
The sixteen tests that clicked the old button by its text are updated,
and it has an id now so its copy can change without breaking them again.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
All three shapes it comes in — youtube.com/watch?v=, youtu.be/, and a Shorts
link — become the video's still with a play button on it and a corner link
out to YouTube. A ?t= keeps its start time. Anything that merely looks like
one (example.com/watch?v=…) stays a link.
The player itself arrives on the first tap, not on render. A feed of ten
videos would otherwise load ten YouTube players, each one telling Google what
you're scrolling past before you've decided to watch anything — and on a
phone, ten players is the scroll. One tap and it plays in place, from
youtube-nocookie.com.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three signed-in identities produced three rows all called "Savings", one
under the other, with nothing to tell them apart — indistinguishable from
duplicates, which is exactly what they looked like. The wallets ARE
per-identity; the screen just never said so.
They're grouped under the person they belong to now: face, name, and a mark
on the identity you're currently signed in as. One identity on the device
gets no heading, because there's nothing to tell apart.
"Whose wallet is this" also becomes one function rather than two: the
accounts screen and the identity switcher were each working it out, and they
have to agree or they describe different worlds.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A push sent to ntfy.sh reached a killed app on a real phone:
17:02:21.259 ntfy.sh → the phone's ntfy → broadcast to io.coinos.webview
17:02:21.308 app process started (it had been killed)
17:02:21.458 wake: job started
17:02:22.926 wake: finishing (page said so) — 1.7s, and "You were paid"
One thing that took two goes: `adb shell am force-stop` marks a package
STOPPED, and Android delivers no broadcasts to a stopped package unless the
sender sets FLAG_INCLUDE_STOPPED_PACKAGES — which `am broadcast` does and a
real distributor rightly doesn't. The first attempt looked like a failure and
was a test artefact; `am kill`, which is what Android itself does to a
backgrounded app, delivers fine.
And the WebView build can now say something useful about push. It was
inheriting the browser's line — "your browser has no push service, try
Firefox" — which is nonsense inside a WebView that IS the app. It asks the
host what's actually there and says: install a push app like ntfy (nothing
installed), or wait a moment and reopen (a distributor that hasn't answered
yet). Payments and messages still arrive whenever coinos is open, which is
the part that matters most to someone reading an apology.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
"Registration failed - push service error" is Chrome's own wording, and it
tells nobody anything. The cause is that Chromium routes web push through
Firebase: on GrapheneOS — or any Android without Google Play Services —
there is no push service to register with, so the app cannot be woken while
it's closed, and no amount of tapping the toggle will change that.
Both places that ask for push (the chat's offer and the NWC "answer while
closed" toggle) now recognise that specific failure and say: this browser has
no push service; on Android that's usually a phone without Play Services;
Firefox brings its own and works there; and zaps are still answered whenever
coinos is open. The last part matters most — the feature isn't broken, it's
narrower than they thought.
The failure is remembered so the app stops retrying it on every launch, and
an explicit tap on the toggle always clears that and tries again, because a
phone that changed browsers deserves another go.
tools/push-unavailable-test.js runs a browser whose subscribe() fails exactly
as Chrome's does and reads back what the user is told.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The attach control was a .btn-sm holding a 16px icon, wedged between two
full-height buttons — a sliver to aim at on a phone. It's a 48px square the
height of the row now, with a 20px clip in it.
And tapping an image opens it full screen: the image centred on a dark
ground, tap anywhere (or Escape) to close. Posts crop images to 320px so a
feed stays a feed, which makes a photograph a thumbnail — this is where you
actually look at one.
Two details in the viewer. The page underneath stops scrolling while it's
open, and the scrollbar gutter the page keeps reserved stands down with it:
fixed positioning is sized to the viewport WITHOUT that gutter, so an
"inset: 0" backdrop leaves a pale stripe down the side of an otherwise
full-screen picture.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The balance read "US$74.67 USD" — the symbol and the unit tag saying the same
thing twice. Amounts now format bare, like sats and BTC do: whatever labels
the number says which money it is. The currency symbol stays where a number
stands alone in prose, which is the "Worth then" line and nowhere else.
The '~' marking a payment older than any price we sampled is gone too. It was
bookkeeping the beta doesn't need: a payment we have a price for is shown at
that price, one we don't is shown at today's, and nobody has to learn a
squiggle.
The currency picker has moved to sit beside the language picker at the foot
of the page — display preferences together, where they're findable. It spent
one day under Settings → Network, where it wasn't.
And a bug this feature exposed rather than caused: nine notices end in the
word "sats" in every language we ship, and were being handed the display
unit. "Uncooperative exit costs about US$2.03 sats" — and long before fiat,
"about 0.00435397 sats" for anyone in BTC. They're given sats now, which is
what they claim to be showing.
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A zap took a few seconds to pay and its public receipt trailed that by
seconds more, so the tally chip appeared long after the tap — the tap looked
ignored, and the only way to tell it had worked was the toast.
Now the chip appears on the tap carrying your amount, breathing while the
payment is in flight; it goes solid the moment the payment lands, and steps
aside when the real receipt arrives (which is also how it avoids counting
your zap twice). The ark and Lightning one-tap flows report their outcome
back — a failure, or a fallback into the form, takes the chip straight off
again rather than leaving it pulsing. An unreported zap gives up after 45s.
A zap sent from the form reports too, so the tally shows it right away
instead of waiting on a relay. Reduced-motion gets a dimmed chip, no pulse.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Chat and DM message sheets grow a ⚡ Zap item (not on your own messages),
riding the same one-tap flow as a post zap — Ark first, Lightning fallback,
the amount remembered from the first time. A chat message is a rumor with an
id like any event, so the zap e-tags that id; the public receipt reveals
nothing but the id.
Every post row, chat bubble and DM shows how much it's been zapped as a
little amber bolt + sats chip (1.2k / 21k / 1.5M), fed by the public receipts
that e-tag it: NIP-57 kind 9735 (amount from the bolt11, the zap request's
amount as fallback) and coinos' own Ark receipt, kind 9737 (network-checked).
Summed per id, deduped by receipt; underlined when one of ours is in it.
Ids are asked once each (batched per paint, 150 per REQ), and ONE live
subscription follows the ~200 ids most recently on screen so a zap landing
while you watch — yours included, rechecked at 4/12/30s — shows up without
a reload. The tally chip is itself a zap button on others' messages.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01URmfo8VimCNkw6Y7JndYrv
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The Accounts screen is about a seed's Spending/Savings faces — its rows say
nothing about WHO each pair belongs to, so a "Sign into another account"
button there was the wrong door. It's gone from there. On your own profile,
"Sign into another account" now opens a switcher: every identity already
signed in on this device as an avatar + name row (one per seed, wearing the
nostr key its header avatar shows — the login key when there is one, else
the seed's own; remembered on the account and in the durable directory by
xpub, so vault-restored accounts have a face too), tap to switch with nothing
logged out, plus "Sign into a new account" for the additive sign-in doors.
With no other identity on the device the button goes straight to those doors.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01URmfo8VimCNkw6Y7JndYrv
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
| |
The only way to move between identities (e.g. Adam via a Nostr remote signer and passkeyguy via passkey) was logout → forget all data → sign in again, which wipes every account. But the machinery to hold several accounts at once already exists: enterWallet/addOrGetAccount ADD an account (they don't replace), the unlock screen carries the sign-in doors (Nostr/passkey/Google via unlockExtra) plus a fromWallet Back button, activateAccount cleanly tears down and re-inits per-account nostr-login state, and the Accounts screen already switches between saved accounts. The nostr-login handoff even saves its session AFTER activating the new account, so each identity's signer state lands under its own namespace and resumes silently (persisted passkey sk / bunker session).
The missing piece was a non-destructive entry point. Added a "Sign into another account" button on the Accounts screen that opens the unlock screen in additive mode (ui.fromWallet), so signing in adds the identity alongside the current one — then switching is just tapping a row on the Accounts screen (reached via the balance card's Accounts button), no logout, no data loss.
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
|
| |
|
|
|
|
|
|
|
| |
(1) The auto-pay opt-in on a fixed-amount merchant pay was a single checkbox that stored "up to the amount you just paid". Replaced with preset budget buttons — 1000 / 5000 / 10000 / 100000 sats — so you set the ceiling deliberately; the chosen amount is what's stored on a successful pay.
(2) An NFC tap into an already-armed auto-pay briefly flashed the review/confirm card (the "Getting a quote…" spinner) before advancing to pay. startArkLnPay now decides up front — from the invoice amount and the stored budget — whether it will auto-pay, and the view renders a centered paying screen straight away (matching the paid screen's layout so it morphs cleanly into it), never showing the review. You just see paying → paid.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
ephemeral-key (v2) gifts.
The old on-chain gift presigned a SIGHASH_NONE/SINGLE spend of the sender's own coin — a blank cheque whose claimer output isn't committed, so anyone (from the link, or a mempool watcher once a claim broadcast) could redirect the presigned input to their own address via a higher-fee tx. A sweeper bot had been front-running coinos gift links for ~a month; it drained a real 5000-sat gift ("claimed! → empty wallet"). Root-caused via chain + code forensics: theft tx input0 sighash = SINGLE.
New design (v2, lazy ephemeral key): the sender presigns a FULLY COMMITTED (SIGHASH_ALL) funding tx paying the gift to a throwaway key E, and puts E's private key in the link. The claimer broadcasts the funding tx, then a fresh SIGHASH_ALL sweep of E into their own wallet. Both txs commit to every output, so neither is redirectable in the mempool — redirecting needs E's key, which lives only in the link (the intended bearer property: holding the link is the only way to claim). Lazy: the funding tx isn't broadcast until claim, so an unclaimed gift moves no coins and is revoked for free by spending the still-reserved funding coin. Sender pays the funding fee (from change / the swept total); claimer pays the sweep fee (from the gift).
- gifts-wallet.js: _buildGiftPsbt now builds+signs the ephemeral funding tx (coin -> E + change, SIGHASH_ALL); new GIFT_MAGIC_V2 codec (parseLazyGift/_encodeLazyGift: [magic][E priv:32][le32 len][funding tx]); giftClaimTxs returns the {funding, sweep} pair; giftOutpoints/previewGift read E's output; isRetiredGift flags the old v1 (compact + legacy PSBT) links. Fee threaded through _selectGiftCoins/createGift/createGiftAll/createGiftFromSplit.
- gifts.js doClaim: broadcast funding (tolerating "already in mempool" from a co-holder/retry) then sweep; refuse retired v1 links with a clear message; verifyClaimWon now watches E's output.
- i18n: giftChainWarn reworded (safe bearer link funding a fresh on-chain wallet); giftRetired added.
Verified: focused crypto test confirms codec round-trip + both inputs signed SIGHASH_ALL (committed). Full regtest end-to-end: funding + sweep both relayed (child accepted spending an unconfirmed parent), mined, claimer address credited the gift. UI morph smoke passes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
(Savings) gift creation now shows a hard bearer-risk warning: the presigned input is redirectable, a sweeper bot is actively front-running shared links, so share privately and claim immediately (Spending/ark gifts are unaffected). (2) The false "success" is fixed: a broadcast returning OK was never proof WE won — a higher-fee sweep can evict our claim (the "claimed! → empty wallet" the bot produced). verifyClaimWon now polls the funding coin after broadcast and, if it settles spent by a txid that isn't ours, retracts the celebration and shows the already-taken screen. Full architectural fix (ephemeral-key gifts, non-malleable SIGHASH_ALL claim) is next
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
steps. (1) Terminology: a payment is a "zap" only when it targets a nostr person or note (npub / note-zap with an eventId, captured as z.social at begin); paying a lightning address, LNURL or merchant now says "Pay over Lightning" / "Pay" everywhere — amount screen, review title, success screen — even when the endpoint supports zap receipts. (2) A fixed-amount payment (min==max — a POS tag, paywall, bolt card) has nothing to type, so the amount screen is skipped and the flow goes straight to the quote/review; the two perceived confirmations collapse to one. (3) Per-recipient auto-pay budget: the review offers "Auto-pay <merchant> up to N next time" (ctx.lnBudget/setLnBudget in prefs, synced), and a fixed charge within a set budget pays as soon as the quote lands — no tap. Guarded: only fixed amounts, only within the budget, only when funds cover it; over budget / no funds / quote error all fall back to the normal review, and social zaps are untouched. Verified with a driven-feature harness (fake ctx + local LNURL server): fixed skips to startLnPay with social=false/fixedSat set, a covering budget sets autopay, variable still shows the amount screen titled/buttoned "Pay"
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
cards hide behind a merchant designation — a one-line "I'm a merchant — show point-of-sale tools" toggle at the foot of the payment-address card, swapping to "Hide point-of-sale tools" once on. The flag lives in the names feature state, so it follows the account to every device via wallet sync; regular users just see one quiet line instead of two cards of terminal plumbing
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
| |
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Registrar: the owner mints a bearer POS token from Settings (NIP-98
POST /pos/token, DELETE revokes; /admin/pos/token for operators). With it
the terminal gets /pos/me, rings up sales at /pos/invoice (fiat via
coinos.io rates, or sats), reads /pos/payments, and holds a websocket at
/pos/ws that gets a payment message when the sale settles. Sales are
always minted on our node so the settle loop sees them and the ordinary
forward path delivers the sats to the name's ark address. The name's
lnurlp pins the newest unpaid sale under five minutes old — coinos.io's
/p/<name> rule — so an NFC tap or a scan pays exactly that invoice.
Wallet: a "Point of sale" section on the names card creates, copies and
revokes tokens.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
|
| |
|
|
|
|
|
| |
gone (no sp in the feature registry) but its mentions lingered: the zapstore description now says plain "full BIP84 bitcoin wallet" (fresh kind 32267 republished, verified clean on the relay), the README loses the sp feature section and its mentions in the E2E-suite and dev-proxy lists, the dead receiveSpTab i18n key is removed, and two stale comments (app.js seams, build.js HAL_FEATURES list) stop naming it. Deliberately KEPT: silentPaymentNote — the send review still labels an output paying someone else's sp1 address, which remains supported
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
sats" (unit included) instead of "exit 3,688", the renewal price moved off the row's right edge into a chip like its siblings — "renewal cost 117 sats" (or "renewal cost free") — and the expiry chip carries a word beside its hourglass ("⏳ expiry 21d"). The depth chip is gone: hop count is implementation detail nobody needed to weigh, and the exit cost already prices its consequence. depthOf and the depth strings retired with it
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
— it was hidden on your own page), bio, and latest posts, exactly like anyone else's, with the editor behind an explicit Edit profile button (Save or the new Cancel returns to the view) and a New post button that unfolds a composer — publishPost signs a plain kind-1 with the login/wallet identity, publishes to the identity's own relays, and prepends it to the notes list so it shows immediately. The header avatar's tooltip says "Your profile" instead of "Edit profile" to match. Compose/edit state clears on open/back so drafts never leak across profiles. Verified headless end-to-end: onboard → avatar → view mode (buttons + npub, no form) → Edit opens form → Cancel → New post opens composer
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
bubble opens a centered sheet: a quick row of the six reaction emojis (your current one highlighted; picking fires immediately) above Reply / Copy / Delete (delete on own messages only) — replacing the hover-only 🙂 picker, which was invisible on the phones that need it most. Links, images and the hover × keep their own clicks, and a desktop text-selection drag never pops the sheet over the copy. REPLIES are new: Reply arms a composer bar showing who/what you're answering (× to cancel, cleared on channel/room switch), the sent kind-9 e-tags the quoted message, and rows carrying an e-tag render the quoted context above their text — author + one-line snippet, silent when the original is deleted or not held. The public /chat page renders the same quotes. Verified against the built stylesheet: 300px sheet, six even emoji buttons, quote ellipsizes, circular cancel button
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
DM bubbles now run through noteBody (URLs → anchors, image URLs inline, npub mentions clickable, all content kept as text nodes — nothing from a relay reaches innerHTML); the public /chat page gets its own linkedBody (same splitter, nostr refs stay dim stubs since there are no profile screens there). Reactions: a 🙂 opener on every bubble (rides the edge like the delete ×, steps left on own rows) unfolds a six-emoji picker; tapping an existing chip joins that vote, your own reaction is highlighted, and picking a different emoji replaces it (kind-7 rumor on the channel stream, one per author, fold keeps the latest — inbound rendering already existed, this adds the sending half). Optimistic like sendMessage: the chip lands instantly and is rolled back to whatever stood before if signing/wrapping fails
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
that shows the live chat history to anyone, logged in or not, with a "Sign in to chat" button ("Open coinos" when the device already has an account). No server-side decoder needed: the community's join material was already public by construction (baked into the client bundle — every coinos user is meant to be a member), so the page derives the stream keys and decrypts wraps client-side exactly like the app's Chat tab, worker-offloaded with the same sliced fallback. Renders messages + edits + deletes + reactions and honors the folded control state's ban list; presence/typing/guestbook deliberately omitted. Profiles come from ONE batched kind-0 query per burst (a REQ per author tripped relay concurrency limits). Plumbing: the join material moved to src/community.js (shared with messages.js so they can never drift), /chat is excluded from the single-segment profile deep-link (which was eating the path and rewriting the URL before the route ran), and app renders stand down in public-chat mode so a feature-init render can't paint onboarding over the page. Verified headless against the live relays: 59 messages decrypt, ban-fold applies, names resolve
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
coin id — the folded row rides ui state (and nav history) since it isn't a stored movement. Ordinary send details also stop hiding their inputs: 'Coins spent' lists what a payment gathered (up to 24), beside the destination vtxo it minted
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
— an ordinary send row, because it was the user's own spend and nobody needs an alarm about which device made it. (The ASP's status API returns only a spent bit, so the shown time is when this wallet caught up)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
row is now a plain Sent wearing a quiet 'synced' tag, and tapping it unfolds the reassurance — the coins left your wallet in a session this device didn't see (another device, a restore that dropped history, or a server-side renewal), and the balance is correct; the row just keeps the history adding up
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
ASP's service charge (base + lifetime ppm) record separately and the history detail shows them on their own lines — settling the mystery of a ~350-sat chain fee inside a ~650-sat total. The community row's member count anchors to last session's settled figure instead of ticking upward through the guestbook replay, and a profile's Pay button reached from search actually reaches Send — the lingering search screen was winning the router
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
button or finger is still down (release is the choice, shared settle logic for mouse, touch and glide) — and the coins table's dotted meta line becomes little labeled pills: an hourglass countdown, depth, and exit cost each in their own chip, the expired one wearing the warning color
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
emits standalone.html (~950KB, everything inlined, boots from a bare file:// with no server), How it works grows a 'Keep a copy' card linking the download, and the tab strip's phantom double-tap dies — the pointerdown's paired click was arriving after the render had already swapped in fresh handlers, and with toggling tabs that ghost click un-did the tap it belonged to. The timestamp guard now lives on the DOM node, which survives the morph
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
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
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
route and re-attempts with a bumped fee budget now says so ('Route failed — retrying with a bigger fee budget (2/3)') instead of three-plus minutes of silent 'Paying invoice…' — a tester paying an unreachable node watched that spinner stonewall through every auto-retry
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
of spendable coins (the single-coin gate silently declined anything over the largest coin, though multi-input pays gather 24), an uncoverable invoice now owns its paste — board the shortfall from Savings, or say what's short — instead of dropping the bolt11 into the address field like a dead click, and chain calls gain a last-resort relay through the registrar (/esplora passthrough, tip cached) for the network that blackholes every public esplora
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
construction hat joins the catalog (never listed for sale — like the crown it appears only in a rack that holds it), the registrar grows POST /hats/grant behind the admin token (hex or npub, crown excluded), and a gifted hat settles into the rack instead of knocking a paid hat off a bare-headed recipient wears it at once
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
the legacy avatar and banner and fills the kind 0's empty slots (fill, never overwrite — an existing nostr identity keeps its own look), the wizard's punk picker steps aside for a profile that already has a picture, and v3 finally wears cover photos — nostr's standard kind-0 banner renders full-bleed across the top of every profile card with the avatar overlapping it, settable by URL or upload in the profile editor
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
confirmations' comes from the ASP's requiredBoardConfirmations instead of a hardcoded 1 that contradicted the 0/3 counter under the balance
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
of '1 coins totalling'
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
coin subset must clear the server's 330-sat output floor by itself, so it now pulls in the smallest healthy coins when the expiring ones alone can't mint an output (128+5 sats retried a doomed round silently every 30 minutes). The coins page also breathes: .coin rows with hairlines and the big accent checkboxes, the amount full-size with its renewal price on the right, the meta line down to 'expires in 26d · depth 6 · exit fee 3,688', and a coin past its height says 'expired' instead of 'expires 0h'
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
two-line row — the amount, then 'expires 26d · depth 6 · exit fee 1,844 · renew 117' riding inline — so there is no header row and no five-column horizontal budget to blow; the column-header strings retire
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
yourself: undecrypted wraps are kept and retried when a signer comes live (the backfill used to be consumed and dropped whenever the login signer hadn't resumed yet — each device kept a different random subset of history), the inbox also reads the relays our kind-10050 actually advertises, and pasting your own ark address or coinos name into Send now opens the board panel from Savings instead of a silent Spending→Spending self-send; a foreign ark address while Savings is selected says to switch instead of quietly spending the other balance
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
~29 days for 150 sats. Manage' — that yields entirely when the depth advisory is on screen, and the day(s) shrug is gone
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
|
| |
|
|
|
|
|
| |
on-chain by itself, expiry reads as a countdown (14d, 13h) instead of a date, and depth is just the number
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019WboeoetMF3MTmgT7Ew8yi
|