| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
on load.
The exit-depth advisory rebuilt every spendable coin's exit chain to learn
its fee, and each hop's input taproot key ran a musig key aggregation
(several secp256k1 multiplies on uncached points). With 27 coins of depth
up to 86 (1685 hops) that was ~5.3s of solid main-thread work on every
boot, laptop and phone alike — requestIdleCallback only delayed it, and
the Android wrapper hits a full boot whenever the OS evicts the app, so
"reopening the app" froze for seconds.
A fee only needs vsizes, and a taproot output script is 34 bytes whatever
key it holds: vtxoTransactions/transitionInputTxout gain a sizeOnly mode
with stand-in keys, exit.js exposes exitTxVsizes (same chain checks, no
EC, nothing broadcastable), and the advisory + coins page use it. Verified
against the real signed chain on the live coin set: identical vsizes on
all 27 coins, 9s → 26ms.
Also: the sync-slot replay at boot merged the ark snapshot and rewrote
~1MB of state (plus re-adopting it into the manager) twice per boot even
when the union brought nothing. Compare the serialized union to what's
stored and skip the write/adopt when unchanged.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
| |
A wallet has two nostr keys — the seed-derived key and, when signed in, the login identity (extension/bunker/passkey/Google). Sending already used one identity, but RECEIVING/advertising used both: startDMs subscribed kind-1059 gift wraps on '#p' for both keys and fetched both keys' inbox relays, and ensureDmRelayList always published a kind-10050 inbox for the seed key even under a login. Result: a login wallet advertised the seed key as a SECOND DM inbox, so the welcomer greeted both keys (duplicate welcome) and a person's DMs split across two identities — one showed up as if from "another user".
Collapse to a single DM identity: new dmSubKeys() returns just the login pubkey when signed in, else the seed key, and startDMs subscribes/fetches only that; ensureDmRelayList no longer advertises the seed key when a login identity is active. isMe() and dmDecryptors() stay tolerant of both keys so already-received messages still thread and align. Not a privacy fix — nothing ever leaked to a third party (both keys were the user's own); this removes the confusing second self-identity going forward. (Existing cached DMs to the seed key on a given device don't retro-clean; a chat re-sync/forget clears them.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
|
|
| |
_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
|
| |
|
|
|
|
|
|
|
| |
A scanned/tapped/pasted payment (NFC LNURL, a pasted lnurl/lnaddr) briefly showed the recipient form — heading, recipient chip, "finding…" spinner and a Back button — while the LNURL was fetched over the network, before flowing into the pay animation + success. That resolving skeleton read as a form the user had to look at on every tap.
Now a tap carries a `tap` flag (set for the scan/paste/NFC entry points: matchSendText with !typed, lnAddressFallback, offers), and during resolve/invoice it renders a minimal centered paying shell — the same autopay-card the ark pay animation uses (spinner-ring ⚡ badge, no Back button) — so a tap goes straight spinner → animation → success with no form in between. Typed lightning addresses keep the normal form (they still resolve through the send form, not a tap). The resolved recipient name/amount arrive on the next screen; the shared card class makes the handoff into the ark autopay animation seamless.
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
|
| |
|
|
|
|
|
|
|
| |
into Paid.
The armed auto-pay flow flashed a "Paying" title/spinner card that then swapped title, badge and footer for the "Paid" card — too much text change and layout shift for something that completes in a moment. Now the in-flight and success states are ONE structurally identical card (autopayCard): recipient name and amount sit in their final positions the whole time, and the step to Paid is just a class flip. The badge starts as a spinner ring (the ⚡ hidden) and, on success, settles into the green success check with a pop while the ⚡ fades in; the "tap to proceed" footer occupies reserved height so nothing jumps. No "Paying" wording at all — the badge carries the state. Respects prefers-reduced-motion. Manual (non-auto) pays keep their normal Paid screen.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
Reactions were a sibling of the bubble (a separate row under it), so they hung outside the message. Moved the reaction pills inside .chat-bubble, tucked under the text, across all three renderers: community channel messages, DMs, and the read-only public /chat page. The pill background and the "your reaction" highlight border now derive from the bubble's own text colour (color-mix on currentColor), so they read correctly on both the plain bubble and the accent "me" bubble. Tapping a pill stops propagation so it toggles the reaction without also opening the message action sheet.
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
|
| |
|
|
|
|
|
| |
backed-up seed doesn't control. A claim into a fresh wallet reportedly paid a DIFFERENT wallet's address than the seed shown for backup (5000-sat gift claimed in incognito → funds landed at an accumulator address the given seed couldn't derive), because the destination was wallet.receive[0] — which can be a stale address a previous account in the session left behind. Now the claim derives THIS wallet's own address fresh from the active seed (never receive[0]), and a hard guard refuses to broadcast unless that address is provably derivable from the wallet whose recovery phrase is about to be handed over — any future seed/wallet divergence becomes a safe error, not lost money
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
|
| |
|
|
|
|
|
| |
notifications went silent. registerPush reused whatever getSubscription() returned and only ever subscribed fresh when there was none; a subscription made against an OLD VAPID key can never receive a push (the notifier's send fails with a permanent 403, which was never pruned, and the client kept re-registering the same dead endpoint). Now the client fetches the current VAPID key first and, when an existing subscription's applicationServerKey doesn't match, unsubscribes and re-subscribes against the current key — so a device self-heals on next open. And the notifier prunes 403 endpoints alongside 404/410. Deployed nwcpush on cs
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
stops opening the staging PWA. The same seed derives the same nostr pubkey on mainnet and staging, so both PWAs registered push under one pubkey and the notifier — matching only by pubkey — fanned every mainnet payment out to the staging subscription too; tapping it opened staging.coinos.io. Now the client stamps its network on the push registration (pushWatch net: getNetwork()), the registrar declares its own network on the /notify call (CFG.forwarder.network), and nwcpush delivers a payment only to registrations whose net matches — with a rollout-safe fallback: a device that hasn't re-registered yet (net=null) still gets the push, and a caller that omits net still reaches everyone, so nobody goes dark mid-deploy. Deployed names + nwcpush on cs
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
|
| |
|
|
|
|
|
| |
showing it hands the LNURL straight to whatever wallet owns the scheme, no scanning your own screen
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
bech32 with the prefix) — tappable and pasteable anywhere that registers the scheme, including our own Android app — while the QR keeps the bare uppercase form for alphanumeric-mode density
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Nostr
Custom name, point of sale, the terminal's tap-to-pay link, own node and
own domain each get a card on Settings → Payments instead of hiding in
collapsed sections. The one-tap zap amount and the CLINK zap code move to
Settings → Nostr, where the other nostr settings are.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Every background repaint stripped the browser-set `open` attribute (the
fresh render never carries it), collapsing expanded sections, and the
positional child match handed one section's open state to its neighbour
whenever the conditional zap-code section came or went. `open` on a
details is now user-owned unless a render sets it, and nodes carrying
data-key only patch into the same key.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
|
| |
|
|
|
| |
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
|
| |
|
|
|
|
|
| |
text/identifier (the lightning address) replaces the bech32 blob as the payee's name and address on the amount screen, with the text/plain description as fallback. And when the address is a coinos name — whether it arrived as an lnurl's metadata, a typed [email protected], or an intent — the registrar is asked who owns it and the screen wears their AVATAR via the same profileChip the npub zap flow already uses; the pubkey also lets a NIP-57 zap request attribute to the right person. Lookup is fire-and-forget so the amount screen never waits on it
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
community rooms and DM threads (now sharing a chat-page class) drop their card walls and run edge to edge under 520px — and the type steps up a notch: bubbles 14→15px, names 13→14px, timestamps 11→12px. Plus: a fixed-price LNURL (minSendable == maxSendable — bolt-card top-ups, paywalls, static charges) no longer prompts for an amount — resolve() fills it and locks the field via the same fixedSat machinery fixed-amount BOLT 12 offers already use (disabled input, adjusted hints, confirm bypass)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
arrives BEFORE the paying feature is ready — ark connects lazily, so zaps' matchSendText declined and the raw URI fell through into the recipient field as dead text; the intent dispatcher now keeps re-offering the text to the feature chain for up to 20s and sweeps the leftover raw text out of the recipient when a feature claims it (the zap amount screen takes over). (2) When the fallback button did render, it read "⚡ Zap lightning:lnurl1dp68gurn8ghj7…<overflow>" — zapDisplay now names targets like a human: the address when there is one, a shortened bech32 otherwise, on both the send-form button and the amount screen
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
client tag — ['client', 'coinos'], name-only since no kind-31990 handler coordinate is published yet — so other clients show "via coinos". Deliberately absent from everything encrypted (DMs, community wraps): private traffic needs no billboard
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
|
| |
|
|
|
|
|
| |
chunk, so every boot painted bare heads until it arrived — but the core bundle can't carry the hat art. Instead, wrapAvatar now leaves a STAMP per head in localStorage (art + percent geometry, size-independent, capped at 40, cleared when a head goes bare so removals can't ghost), and the core avatar factory replays stamps until hatsReady answers — from then on a null from wrapAvatar genuinely means bare. Verified headless: a stamped hat is on the header avatar 15ms after DOM-ready, and once the chunk lands with a bare-head answer the stamp is cleared with no double-wrap
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
sits INLINE, directly under the note it answers, instead of at the bottom of the thread where nobody scrolled to find it — slotted after the focused note (the root by default), appended at the end only until the focused note loads, with a refocus handshake so the cursor survives the box relocating when the thread finishes loading. (2) Posting is OPTIMISTIC: a provisional dimmed copy tops the feed before any signing or relay round-trip (a remote signer alone can take seconds), swapped for the signed event on success and withdrawn on failure — the composer closes immediately and reopens with the text intact if publishing fails; pending rows can't be opened/replied/zapped until real. (3) The full header nav (search, chat, settings, lock, avatar) now shows on the profile, thread, and search screens whenever a wallet is open — losing the search button on a profile made finding the next person a trek back home; the public no-wallet surface keeps the plain brand. All verified headless: box renders mid-card under the focused note with focus landing after load, a post appears 250ms after tapping Post (pre-publish) and survives settle, and the search button works from a profile
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
the post's thread focused on that note and puts the cursor straight in the reply box (inside an already-open thread it just re-targets the reply to that message). Verified headless on /adam: 51 rows all carry the button, tapping one lands with the thread reply input focused
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
pulls the next page of older kind-1s — until = oldest seen, one fetch in flight, a short answer marks the end; the listener registers at feature construction so the public no-wallet /username pages scroll too), VIDEO rendering in noteBody (mp4/webm/mov/m4v as <video controls preload=metadata playsinline> beside the existing inline images), and FULL-BLEED on phones — the notes card drops its walls and runs edge to edge under 520px; the inner 440px scrollbox is gone everywhere, the page itself scrolls. Verified headless at phone width on /adam: feed spans 0px-to-edge both sides, images render inline, and scrolling grew the list 51 → 82 posts
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
Delete — a gift-wrapped DM can't be retracted from the peer's relays). DM reactions ride the kind-7-in-a-gift-wrap shape 0xchat uses (makeDMReaction in dm.js), sent to the peer and echoed to our own inbox so other devices fold them in; inbound kind-7 rumors land in dmReacts keyed by the reacted message's id — no thread lookup, so a reaction arriving before its message still sticks. Replies e-tag the quoted kind-14 (makeDMRumor grew extraTags) and render the same accent-bar quote as community chat; the composer gets the same replying-to bar. Chips are joinable and your own reaction is highlighted/replaceable, all optimistic with rollback. Round-trip verified: reply and reaction rumors wrap/unwrap with tags intact
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
profile page immediately, not after Save: while the editor is open the DRAFT is the truth for the banner, the avatar, and the display name shown up top — clearing a field previews the removal too. The editor's text fields render per keystroke so pasted URLs track live (the morph never rewrites a focused field, so it can't fight the typing); uploads already rendered after filling the field. Verified headless: typing picture/cover URLs into the edit form updates the page-top avatar and banner backgrounds before any save
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
|
| |
|
|
|
|
|
| |
state) at sync time, newest-first per txid, instead of adopting the newest core's list wholesale. The wholesale apply lost history: a device whose history fetch keeps failing (mobile 429 storms — the balance scan is cheap per-address calls, history is the heavy /txs pulls) publishes a THIN core with txs:[] and a fine balance, and every fresh device then adopted the newest=thin core and showed a balance with no transactions — adam's phone today, with three ~2KB thin cores on the relay shadowing the fat 7KB core from Aug 18 that still holds the history. Union is safe because history is append-mostly and every SUCCESSFUL local scan still prunes rows its addresses no longer report; a wiped device also self-heals the fleet, since its republished core carries the union'd txs back to the relay
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
key is now kept in the wallet's nostrlogin state and resumeLogin reattaches from it instantly, so opening chat no longer greets passkey users with "Your nostr signer isn't connected". Kept deliberately and only for the passkey door: a pasted nsec keeps its used-once-never-stored promise, but a passkey key exists only through this app's PRF derivation and re-deriving it costs a WebAuthn ceremony (system dialog + biometric) on every open — and the same storage already holds the wallet seed, so this is the same exposure class. Existing passkey sessions heal on their next reconnect: that ceremony saves the key, and none is asked for again
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
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)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
the Ark mark: a faucet paying the LN address settles on the registrar's CLN and arrives as an anonymous arkoor from the float (the mailbox protocol carries no memo), so the wallet genuinely couldn't tell the rail. The registrar now records a receipt {sat, hash, ts} per LN settlement it forwards via the float (7-day ring per name) and serves them at GET /lnreceipts (NIP-98, owner/manager only — it's a feed of incoming amounts); the wallet fetches them through the names feature (which holds the signer) and fuzzy-matches receive movements by exact amount + delivery within ±3 min, stamping viaLn + the payment hash so a receipt can never claim a second row. History row and detail view show ⚡ for viaLn receives; the tag rides the sync snapshot to other devices via merge enrichment. viaClink deliveries need none of this — the wallet's own LN-receive flow already tags itself. Best-effort by design: an amount+time collision only mislabels an icon
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
rebuilding it costs ~60ms (sort + dedupe + gift resolve + secp256k1), and its 1.5s TTL meant an unhurried flick always hit a cold rebuild, stuttering the slide's tail — 'warms up if I keep going fast, cold if I pause, refresh reproduces' is the classic TTL-expiry signature. Now keyed on a state revision (_save bumps state._rev) so it's reused until the data actually changes and never re-cools between flicks. Diagnostics still on to confirm
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
unfolded on the balance card with no way to dismiss it short of completing or switching tabs. A centered Cancel link closes it back to the plain card
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
every community room up front (500 control + 500 guestbook + channels each — the bulk of the ~560ms of boot secp256k1 behind the carousel stutter). Now boot builds rooms from the decrypted local cache ONLY (subscribe:false), so the unread dot still reads last-known state, and the live backfill's verify/decrypt waits until the user actually opens Chat (homeView/room view subscribe then). ensureRoom split into cache-build + subscribeRoom
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
every spendable coin and built the exit txs (heavy secp256k1) synchronously on each render, and the cold first compute after a refresh landed right on the carousel drag. Now the render only reads the cached answer; the recompute runs in requestIdleCallback (keyed by coin set + feerate, so it fires only when something changed) and re-renders when done — the EC never competes with input
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
the exact cold first-drag window showed ~700ms of secp256k1, and it was the COMMUNITY room's gift-wrap decryption — a relay backlog of 500+ wraps decrypted inline in the subscription callbacks, one synchronous EC burst on boot (the DM inbox already yielded; community rooms didn't). Now their decrypt work is time-sliced through the same yield-every-8ms queue, so it spreads over a few frames and the wallet stays responsive. Measured: first cold flick dropped 6 frames before, and this was the dominant cost
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
(NIP-44 ECDH, ~500ms of secp256k1 in a boot profile — the last of the carousel jank) ran as one uninterrupted loop. Each synchronous run is now capped to ~8ms, one frame's budget, then yields so input and paint get a turn; the backfill just spreads over a few extra frames instead of freezing them
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
Every _save() — fired dozens of times during a boot reconcile — did a JSON.parse of stored state, a merge over every vtxo and movement, a JSON.stringify, a synchronous localStorage write, and saveCache(). A CPU profile of the first 9s pinned it: ~230ms in saveFeatureState/loadFeatureState/setItem bursts blocking the main thread, which delayed the mouse drag's scrollLeft writes (mouse drag is main-thread; touch scrolls on the compositor). The in-memory state is always current and every live read uses it, so the disk write is now coalesced — immediate when idle, one write ~200ms after a burst settles (800ms cap), flushed on page-hide and wallet switch so nothing is lost
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
back. On open, when Savings is empty and Ark has a balance, the wallet flips you to Spending — a convenience. But it fires on every Ark update and only latches once the balance SYNCS, which after a refresh arrives a second late — right after you've dragged to Savings, so it overrides your choice and slides back. Now any deliberate pick (carousel scroll/tap/flick, swipe, accounts screen) sets accountUserChosen, and the auto-select consumes its one-shot without flipping when you've already chosen
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
a small button in the grouped row with copy tx id / view explorer / copy address — one consistent size for every secondary copy action
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
secondary copy/view action (copy gift link, copy tx id, view on explorer, copy address) now rides one wrapping row at its natural width, rather than each stretching full-width and getting split apart by the block buttons between them. Revoke (now danger-styled) and Back keep the full width they deserve as primary actions
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|