| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The Device sync card and toggle are gone — sync is always on, riding
the user's own NIP-65 (kind 10002) write relays: the login npub's
first, else the wallet key's, coinos relay as the fallback and always
included. A fresh wallet identity with no NIP-65 publishes one
(default relay) so other clients can find its events.
The npub-locked-gift remnants go with it: the per-wallet nostr address
card that pitched gift-locking, the how-it-works sentence, the dead
NostrSync.sendDM/sendNostrDM path, and the orphaned claim-code field.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Tapping any avatar or author name (chat rooms, DM threads, the chat
home) opens a profile page: avatar, name, nip05, lightning address,
about, npub, with Message and Pay actions (Pay runs the npub payment
cascade). Your own profile — reachable from your avatar on the chat
home — gets an editor for name, about, picture, and lightning address
(prefilled with your coinos address); saving merges over the newest
published kind 0 so fields we don't understand survive the round trip.
Zaps now remember who they went to: the ark feature keys the recipient
by what the movement already records (destination address or invoice)
and history rows wear a clickable avatar chip that opens the profile.
The messages feature exposes showProfile/profileChip seams so any
feature can do the same.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Gifts are plain bearer links again: the recipient-npub field, the
one-time claim code, the DM delivery, the /lg/ claim route and the
NIP-07 in-place decrypt are all gone, along with the crypto that only
they used. Unclaimed gifts remain reclaimable by the sender, so nothing
can be stranded by the removal.
The unlock screen shows a single 'Sign in with Nostr' button that
reveals the login options when tapped, instead of the full card
competing with create/import.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One monolithic snapshot per device outgrew a vanilla relay's 64KB event
cap the moment real payment traffic arrived, and sync silently died.
Snapshots now fragment into per-domain replaceable events (core wallet,
ark, nwc — an extension claims a domain by naming it), published under a
new ':x:' d-tag marker that pre-split clients don't match, so they can
never mistake a fragment for a full snapshot. Only domains whose content
changed republish, a refused publish retries on the next save instead of
being marked done, and readers apply full-snapshot semantics only to
core-carrying events. The ark projection also caps its history: spent
stubs at 200, done actions at 50 — the event size is bounded for life,
no special relay rules required.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Tonight's payments pushed the encrypted snapshot past strfry's 64KB
maxEventSize and every publish after the first zap was silently
refused — the other device just showed no history, with nothing logged
anywhere. The relay's limit is raised to 256KB; client-side, movements
older than the last 50 now drop their bolt11/preimage (~half a KB per
ln-send) and the cap tightens to 200, and a refused snapshot publish
logs the relay's reason.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
Loose leader election for pay_invoice across a user's open tabs: before
handling, defer a beat and stay silent if any reply for the request
already exists on the relays; and before sending an ERROR reply,
re-check — a device that lost the race (mid-init, stale state) was
replying 'superseded' after another device's success, and clients
trusted the later error, marking paid zaps as failed.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A silently dropped WebSocket (proxy idle timeout, network change) used
to kill every live subscription with nothing re-subscribing — device
sync stopped merging and the NWC service stopped answering until the
next reload. That is how a connection created on the phone stayed
invisible to an open laptop tab. The pool now pings every 29s (dummy-REQ
fallback in browsers) and reconnects with backoff, re-firing open
subscriptions from the last event seen.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
relay.coinos.io's write policy was rejecting every 23195/13194 hal sent
(kinds were whitelisted to coinos server pubkeys — incompatible with
hal's per-connection service keys) and the rejections temp-banned the
IP. publishOn swallowed all of it, which disguised a relay-side policy
problem as a client bug. The relay policy is fixed separately; here,
refused publishes now log the relay and reason, and the 13194 info
event goes out once per connection per session instead of on every
listen() so replies keep their share of the relay's per-IP rate budget.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
nostr-tools ≥2.23 changed subscribeMany to take one filter object; the
old array form now serializes as ["REQ",id,[{...}]], which every relay
answers with CLOSED "provided filter is not an object". That killed the
NWC request subscription (the Amethyst forever-spinner), the device-sync
live subscription, and the push notifier's relay watch — all silently.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Every guard in onRequest now names itself when it drops an event, and
listen()/stop() log subscription generations so a later listen() killing
the live sub becomes visible. subscribeOn passes extra params through so
relay-initiated CLOSED reasons surface too.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The connection URI advertised up to four relays (sync relays + profile
relays) while the subscription only ever listened on the sync relay, so a
client could publish its request somewhere hal wasn't and spin forever
waiting for a reply. Amethyst did exactly that.
NWC now has its own relay list used for all three things that must agree:
the URI, the subscription, and the replies. It no longer depends on device
sync being enabled, which would otherwise have made the whole wallet service
a silent no-op.
Also republish the kind 13194 capability event on every start — a client that
can't find it just spins, and a single publish at creation time is easy for a
relay to miss.
The relay transport is now injectable so the protocol can be exercised
without a live relay (tools/nwc-test.js).
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Other apps can connect over nostr and pay from this wallet without touching
its keys, within a per-payment and daily budget the user sets, revocable at
any time. hal holds no native Lightning balance, so every operation runs over
Ark: pay_invoice takes the swap bridge when configured and falls back to the
ASP's lightning-send, make_invoice mints on the ASP. NWC availability
therefore tracks Ark availability.
- kinds 13194/23194/23195, nip44_v2 and nip04, replying in whichever scheme
the request used; every request gets a reply so clients never hang
- each connection gets its own service keypair, so revoking one can't affect
the others and none is tied to the user's nostr identity
- budgets are checked BEFORE spending, not reconciled after
- requests are ignored unless signed by that connection's client key, and
replayed/stale events (>120s) can't re-trigger a payment
- Settings card to create, show (URI + QR) and revoke connections, and it
says plainly that this only works while a tab is open
tools/nwc-test.js drives it as a real client: both encryption schemes, budget
refusal with a genuine over-limit invoice, unsupported-method errors, and a
request from an unauthorized key being ignored.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Send a Lightning zap to a nostr npub, a lightning address (name@domain),
or an lnurl. New src/lnurl.js does the resolution half — target → LNURL-pay
params → signed kind-9734 zap request → callback → bolt11 — and the new
zaps feature (src/features/zaps.js) drives the amount/comment UI, then hands
the invoice to the existing Boltz submarine-swap flow via a new
swaps.startLnPay hook, so there's one tested pay/review/confirm/success path.
Routing: a bolt11 is still caught by swaps; an npub still prefers an instant
free Ark zap when Ark can serve it, and only falls through to a Lightning
zap otherwise — ark's "no Ark address" screen also offers an explicit
"Zap over Lightning" fallback (gated on the zaps feature's canLnZap). A real
zap receipt is attributed only when the server advertises allowsNostr and we
know the recipient's pubkey; otherwise it degrades to a plain LNURL-pay.
Nostr seam gains nostrSign (finalize without publishing), nostrProfile
(kind-0 with lud16/lud06), and nostrRelays. Lightning addresses are
debounced in matchSendText since their regex matches mid-typing.
Verified: unit tests for parse/encode/zap-request; a live resolve→signed
9734→payable mainnet bolt11 against coinos; and an in-browser DOM smoke
(zap form + LNURL params over CORS + allowsNostr hint + handoff).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Cross-device ark sync silently failed: the full snapshot (deep-genesis
vtxo bytes + done-action signing hex) was ~153KB and relay.coinos.io
rejects events over 64KB, so no publish ever landed and other devices
never saw the balance. The synced ark state is now slimmed — spent
vtxos drop their signed bytes (kept only for spendable/pending, which a
device might spend/exit), done board/offboard/exit actions keep just
the fields history labels need (hex stripped), in-flight actions and
movements beyond the last 300 are dropped. Local storage keeps the full
state; only the published copy is slim. ~150KB -> well under 64KB.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The clobber was structural: all devices published to one replaceable
event (same seed -> same nostr key -> one slot), so whoever saved last
won and a device saving before it merged wiped the others' coins. Now
each device publishes to its OWN d-tag (<base>:d:<deviceId>, deviceId
random per browser in localStorage), and readers pull ALL of the
identity's kind-30078 events — every device slot plus the legacy shared
tag for migration — merging ark vtxos across all of them and applying
the newest full snapshot for on-chain state. No device can overwrite
another's slot, so anti-entropy is no longer load-bearing (kept as
belt-and-suspenders). Live subscription drops its #d filter to see all
slots; publish and the echo-guard use the per-device tag.
Verified: two same-seed devices produce two distinct relay slots that
coexist, B sees A's funds, both converge after a spend.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Ark vtxos lived only in per-device storage, so a second device with the
same seed saw the right on-chain balance (that rides the snapshot) but
none of its ark funds — board/change vtxos exist only where created.
Ark state now rides the encrypted nostr snapshot as a merge-safe cache
extension: additive union (bring in vtxos a device is missing) with
each device's own server reconcile() as the authority for spent-ness,
so a stale or wrong 'spent' on one device can never propagate and
poison the others. A connect-time reconcile catches coins spent
elsewhere while a device was away, and a live subscription to our own
replaceable event merges another device's new coins within seconds.
New seam: registerCacheExtension({mergeAlways:true}) extensions get
fed snapshots that aren't newer than local state too (last-writer-wins
is right for rescannable chain data, wrong for merge-style vtxo state),
and nostr.subscribeStates for the live path.
Verified two same-seed devices over a mini relay: B sees A's boarded
balance with zero mailbox activity, spends a synced vtxo, A converges
to a stable balance with the spent vtxo correctly marked.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The compatibility play for ark zaps: an NWC wallet service (coinos,
eventually) receives pay_invoice from any nostr client, asks the hal
wallet to cover the amount over ark (kind 23196 funding request), hal
auto-pays within a user-set allowance and acks (23197), the bridge
receives the vtxo in its mailbox and settles the bolt11 over
lightning. Custody window is the seconds the sats are in flight.
hal side: a live nostr subscription primitive (nostrSubscribe through
the sync seam) and an allowance-gated funding handler that arms when
ark connects. PoC config via localStorage 'btc-wallet-arknwc'
({bridgePk, budgetSat}); proper UI when the protocol settles.
Proven end-to-end on regtest (tools/.arknwc-e2e.tmp.js): mini relay +
bridge process (ArkManager in bun + CLN) + NIP-47 client — invoice on
clb paid via cl, preimage returned over NWC, hal debited 1,000 sats
over ark, zero page errors.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Lightning zaps are welded to LNURL+bolt11; nutzaps (NIP-61) proved the
alternative shape, and ark fits it better than Cashu because delivery
already exists (the mailbox). Draft protocol, kinds provisional:
kind 10037 (replaceable) announces the wallet's ark address+network;
the zap itself is a plain instant arkoor send; kind 9737 is the
sender-signed receipt tagging recipient, amount, comment, and the
delivered vtxo id — so the receipt is checkable against the actual
payment, unlike a bare attestation.
UX: paste an npub into Send and hal offers the zap (needs both the ark
and sync features; npub decoding is inline bech32 so ark-only builds
stay lean). The nostr layer gains generic publishEvent/fetchEvents,
exposed through the sync seam as wallet.nostrPublish/nostrFetch.
Proven on regtest against a mini NIP-01 relay: announce, resolve,
zap 1,000 sats with a comment, instant receipt+delivery, vtxo match.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
One shared d-tag let a network switch apply the mainnet snapshot to a
signet wallet (the flash of wrong balance) and clobber mainnet state on
the relays. Each network now syncs its own replaceable event (mainnet
keeps the legacy bare tag), snapshots carry netName, and a foreign-net
snapshot is never applied. The balance card spins until cache, Nostr,
or a scan has populated this network once — 'scanning' alone missed the
cache/Nostr window after wallet.load().
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
| |
Stop also sending the legacy NIP-04 kind-4 DM — clients that support both (e.g.
Amethyst) were showing the claim code twice. NIP-17 alone covers current clients.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Migrated all relay I/O (profile/inbox fetch, DM publish, state sync) to a shared
SimplePool. The old code opened a relay per request and closed it manually,
which raced nostr-tools' CLOSE-after-EOSE and spammed 'WebSocket is already in
CLOSING or CLOSED state'. SimplePool owns the connection lifecycle, so that's
gone. Also reuses connections instead of churning one per fetch.
- Gift DMs now go out as BOTH NIP-04 (kind 4) and NIP-17 (kind 1059 gift wrap).
Older clients (Amethyst) read NIP-04; newer ones (Jumble) only support NIP-17
and ignore NIP-04 — sending both means the claim code shows up either way.
Verified: profile/inbox fetch + DM still work via the pool; the NIP-17 wrap is
published and unwraps to the original message; the NIP-04 kind-4 still lands too.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The gift payload is now encrypted two ways in the link: under the one-time code
(the DM / manual path) AND to the recipient's nostr pubkey via an ephemeral key.
When a NIP-07 browser extension (window.nostr) is present, the claim view offers
'Claim with nostr extension' — it checks the extension's pubkey matches the gift,
decrypts ctKey via window.nostr.nip44.decrypt, and claims into a fresh wallet with
no code. The pasted-code path remains as the fallback for everyone else.
Verified: both decrypt paths recover the payload from the blob; a different
extension account is rejected.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Discover where a recipient reads DMs before sending the claim code: prefer their
NIP-17 DM relay list (kind 10050), else their NIP-65 read relays (kind 10002),
unioned with the broad fallback set (so users without a list still get it). This
makes the locked-gift DM far more likely to land in the recipient's client.
Verified: resolves fiatjaf's real inbox relays; a key with no list returns none
and falls back to defaults.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A gift can now be locked to a recipient's nostr npub. The shareable link
(/lg/<blob>) carries the amount + recipient in the clear (anyone can see what it
is and who it's for), but the claim payload is encrypted under a one-time code.
That code is delivered as an encrypted nostr DM (NIP-04) to the recipient — so
only they can read it. They paste the code to decrypt, then claim into a fresh
wallet exactly like a normal bearer gift.
- nostr.js: parseNostrPubkey/npubOf, encryptWithCode/decryptWithCode (random
32-byte code), fetchNostrProfile, NostrSync.sendDM (broad relays).
- wallet.js: lockGift → { blob, claimCode }, previewLockedGift, nostr identity +
sendNostrDM.
- app.js: gift form gains an optional 'Lock to npub' field that shows the
recipient's avatar + name (nostr kind-0 profile); create encrypts + DMs the
code (with sent/failed status + a copyable fallback); /lg/ links open a claim
view showing the gift + recipient where you paste the code; account settings
shows your own npub to share.
Verified: code lock/unlock round-trip, a real nostr DM published and read back
from relay.damus.io, and the UI loads + renders with no errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Device sync:
- Add a "Device sync" settings card: toggle (on by default) + editable
relay list (defaults to wss://relay.coinos.io). When off, the wallet
never contacts a relay. nostr.js is now multi-relay and reads an
enabled/relays preference from localStorage.
Reconciliation (fixes a stray "payment received" + stale history on
import, especially via the Nostr/cache load path):
- Always full-scan on load instead of a frontier-only refreshLive, so
balance, history confirmations and spends are reconciled and the
celebration baselines against everything that already exists (cache/
Nostr still shows instantly first).
- Actually start the periodic full-scan safety net (_deepTimer, 2 min);
it was declared but never wired up.
- WS pushes now refresh an existing tx's pending→confirmed status, not
only add new transactions.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
|
|
Persist wallet state (addresses, utxos, history, indices, fees) as a NIP-44
encrypted, parameterized-replaceable event (kind 30078) addressed to self on
relay.coinos.io. The Nostr identity is derived from the same seed (NIP-06), so
no extra secret. On load we pull the latest state from Nostr — a device with
no local cache gets full state without re-scanning, and another device's
updates propagate — using savedAt to keep the newest copy. localStorage stays
as the instant/offline cache; state is published (debounced) on every change.
(Relay note: added kind 30078 to the strfry write-policy allowlist on prod.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|