aboutsummaryrefslogtreecommitdiffstats
path: root/src
Commit message (Collapse)AuthorAgeFilesLines
* Drop npub-locked gifts; collapse Nostr login behind a buttonAdam Soltys2026-08-016-208/+26
| | | | | | | | | | | | | | 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]>
* Stop the Ark card flickering on the settings pageAdam Soltys2026-08-011-2/+15
| | | | | | | | | | | | Every synced snapshot re-ran initArk() while Ark was unconnected, and initArk() calls stopArk() first — so each arriving snapshot tore down and restarted the connection, flashing 'Connecting to the Ark server…' and jittering the page. Automatic re-inits are now skipped while a connection exists or is in flight, and rate-limited to one per 20s; user-driven reconnects (switching servers, the connect button) are unaffected. Co-Authored-By: Claude Fable 5 <[email protected]>
* Restore two helpers an earlier edit deleted, and guard the class of bugAdam Soltys2026-08-011-0/+14
| | | | | | | | | | | | | | A bulk replacement removed lookupMine() and the flag along with the block it was rewriting, so names setup threw at runtime — the kind of break every existing test happily passed over because nothing ever constructed a feature and started it. tools/features-smoke-test.js does exactly that: builds every feature against a stubbed wallet with the network cut, runs init/render/stop, and fails on undefined identifiers. It caught the second missing helper immediately. Co-Authored-By: Claude Fable 5 <[email protected]>
* Get an address immediately, without waiting on a signer promptAdam Soltys2026-08-013-25/+33
| | | | | | | | | | | | | | | The passive setup path asked the browser extension to sign the claim, so it sat behind a permission popup — 20s per attempt, retried, hence the minute-long spinner ending in the manual form. The wallet's own key now makes that claim: no signer, no prompt, no wait. Tying the address to the user's real npub happens at login instead, where the signer is already live, and nominates the wallet key to manage the record after. Failures are visible now rather than hidden: the pane shows why setup failed, the watchdog is 12s instead of 60s, internal timeouts are tighter, and the registrar logs every refusal with its reason. Co-Authored-By: Claude Fable 5 <[email protected]>
* The real cause: the CORS preflight rejected our own auth headerAdam Soltys2026-08-011-8/+21
| | | | | | | | | | | | | | | | NIP-98 puts the claim under an Authorization header, but the registrar only allowed content-type in access-control-allow-headers, so browsers blocked every claim before sending it. The failure arrived as a generic network error, which the retry logic treated as transient — hence the endless 'setting up your payment address' spinner. Bun tests never saw it because they don't preflight. Registrar now allows the header. Client-side, nothing may hang: signing, the registrar call and the ark lookup all have timeouts, retries are capped, and a one-minute watchdog always resolves the spinner into either an address or the manual form. Co-Authored-By: Claude Fable 5 <[email protected]>
* Never spin on a claim that can only failAdam Soltys2026-08-012-6/+36
| | | | | | | | | | | | After a reload the extension signer is gone, so claiming the user's real npub name was signed by the wallet key — which the anti-squatting rule rightly refuses, and the retry loop then span forever. An installed extension is now silently re-attached when possible (keeping the address tied to the real identity across reloads), refusals are treated as permanent rather than retried, and anything unresumable falls back to the wallet's own npub so an address always appears. Co-Authored-By: Claude Fable 5 <[email protected]>
* Actually assign the default payment addressAdam Soltys2026-08-012-3/+23
| | | | | | | | | | The auto-claim ran once at feature init — before Ark finishes connecting — saw an unavailable wallet and gave up for good, so users got a claim box instead of their npub address. It now retries while Ark comes up (and after a failed claim), and shows a setting-up state rather than the claim form while it works. Co-Authored-By: Claude Fable 5 <[email protected]>
* One wallet per Nostr identity, NIP-98 auth, npub payment addressesAdam Soltys2026-08-014-52/+89
| | | | | | | | | | | | | | | | | | | | | | | | | | Consistency fix with teeth: logging in with a pasted key derived one wallet while an extension login for the SAME identity found no backup and generated a different one. Now every signer kind runs one flow — look for the published association first (that's where the money is), then derive (key in hand) or generate (signer only) — and a derived seed is published too. That costs nothing, since the derivation is public and anyone holding the key could compute it anyway, and it makes the identity open the same wallet however you log in. Registrar auth is now NIP-98 (HTTP Auth, kind 27235) instead of a homemade envelope: the signature binds the URL, method and body hash, so a captured header can't be replayed elsewhere. NIP-78 already covered the encrypted association; NIP-26 delegation is deprecated, so owner→manager nomination lives in the registrar's own model: an npub-shaped name may only be claimed by that identity, which nominates the wallet key to keep the record updated afterwards (the wallet key is the only one always available — offline, in the worker). Payment addresses therefore default to the user's REAL npub in all three login methods, falling back to the wallet's own identity when no Nostr account is linked. Co-Authored-By: Claude Fable 5 <[email protected]>
* Log in with Nostr, and link a wallet to a Nostr accountAdam Soltys2026-08-015-1/+350
| | | | | | | | | | | | | | | | | | | | | | | | | Three ways in: a NIP-07 browser extension, a NIP-46 remote signer (Amber, Amethyst — paste its bunker:// address), or a pasted nsec/hex key that lives in memory for the session and is never stored. How an account gets a wallet depends on what we can see, and the two cases are genuinely different: * We hold the key (pasted nsec): the seed is DERIVED from it — sha256("coinos:wallet-seed:v1" | privkey) as BIP-39 entropy. The same Nostr key always opens the same wallet, on any device, with nothing stored anywhere. * We only have a signer: deriving is impossible (we never see the key, and neither signatures nor NIP-44 ciphertexts reproduce), so the seed is ASSOCIATED instead — generated once and published as a replaceable event encrypted to the user's own key, which only their signer can decrypt. Later logins find and decrypt it. The second path means the Nostr key can recover the money; the UI says so before publishing anything. Linking an already-open wallet uses the same publish path and refuses to overwrite an account's existing wallet. Co-Authored-By: Claude Fable 5 <[email protected]>
* Default usernames from the npub — no onboarding promptAdam Soltys2026-08-012-17/+25
| | | | | | | | | | | | | Every wallet silently claims the first 20 characters of its npub as its payment address the first time it opens: unique, free, and nothing to decide. The pick-a-username gate is gone (the generic onboardingView hook stays as an extension point). A custom name is an optional change tucked into the payment-address card — and swapping to one releases the old name so the registrar and DNS stay tidy. Custom names may become paid later to deter squatting; the default tier keeps working regardless. Co-Authored-By: Claude Fable 5 <[email protected]>
* Username onboarding + the payment address leads ReceiveAdam Soltys2026-08-013-26/+101
| | | | | | | | | | | | | | | New wallets must pick a username before entering the wallet: the onboarding gate renders a claim screen until a name exists. Imported seeds skip it — the registrar's new /pubkey lookup recovers the name owned by the wallet's key, so nothing is retyped. If claiming errors (registrar or ark unreachable) a do-it-later escape appears rather than locking anyone away from funds; the prompt returns next open. The Receive tab now defaults to the payment address — name QR + copy — with the on-chain address and other modes behind it, and the claim form inline for wallets that skipped onboarding. Co-Authored-By: Claude Fable 5 <[email protected]>
* Bigger header logoAdam Soltys2026-08-012-3/+3
| | | | Co-Authored-By: Claude Fable 5 <[email protected]>
* Fix the logo symbol: drop the background disc pathAdam Soltys2026-08-011-1/+1
| | | | | | | With the root filled currentColor, the disc covered the ring and half-moon; the drawn symbol needs only the outline paths. Co-Authored-By: Claude Fable 5 <[email protected]>
* Use the official coinos wordmark SVG in the headerAdam Soltys2026-08-012-6/+3
| | | | | | | The full logo from coinos.io (symbol + wordmark as drawn paths) at 28px, inheriting the ink color for both themes; replaces the mark + text pair. Co-Authored-By: Claude Fable 5 <[email protected]>
* The coinos mark in the headerAdam Soltys2026-08-013-4/+3
| | | | | | | Inline single-color SVG of the brand symbol (ring + half-moon) that inherits the ink color, next to the lowercase wordmark. Co-Authored-By: Claude Fable 5 <[email protected]>
* Fresh-seed restore: connect after parking a synced ark snapshotAdam Soltys2026-08-011-0/+6
| | | | | | | | | | | | A seed imported on a new device (or a new origin — v3.coinos.io) has no local ark state and no live connection, so the anti-poisoning guard can't prove the incoming snapshot's owner and parks it in the hold. Nothing then triggered the connect that would adopt it — ark is lazy on fresh wallets — so the balance never appeared. Parking with no local identity now kicks the init; connecting learns the server pubkey and the held state is adopted iff it matches. Co-Authored-By: Claude Fable 5 <[email protected]>
* Grayscale button hover — the last orange hardcodeAdam Soltys2026-08-011-1/+3
| | | | Co-Authored-By: Claude Fable 5 <[email protected]>
* Coinos branding: logo and black & white themeAdam Soltys2026-08-014-11/+13
| | | | | | | | | Icons, favicon, and PWA assets come from the coinos brand set; the accent goes monochrome (black in light mode, white in dark, with a proper on-accent text variable so buttons stay legible both ways) and the inline bitcoin mark inherits the text color. Co-Authored-By: Claude Fable 5 <[email protected]>
* Coinos v3: rebrand from Hal, payment addresses move to @coinos.ioAdam Soltys2026-08-015-17/+17
| | | | | | | | | | | | | | | The wallet is the next version of coinos, deployed at v3.coinos.io (same build as halwallet.app) ahead of an eventual migration of coinos.io itself. App name, manifest, notifications, and the NWC alias all say Coinos; the Hal Finney tribute stays. The registrar is multi-domain (per-domain Cloudflare zones; existing halwallet.app names keep working) and new claims default to @coinos.io. Names belonging to existing coinos users are reserved for them — the registrar checks the live coinos API before granting a claim — until the migration gives those users a way to claim their own. Co-Authored-By: Claude Fable 5 <[email protected]>
* Zappable offer codes and per-name Lightning offersAdam Soltys2026-08-0110-80/+699
| | | | | | | | | | | | | | | | | | | | | | | | | CLINK offers (noffer): every wallet gets a static offer keypair (synced across devices, first-minted wins) answering kind-21001 invoice requests — from open tabs and from the service worker while closed, which mints against the mirrored state (the record now carries chain-4 and chain-5 key windows) and raises an open-to-receive notification. Zap requests ride along and their kind-9735 receipts publish once the payment is claimed. The code is shown in the payment-address card for pasting into a Nostr profile; the notifier watches and relays kind 21001 alongside NWC. Payment names also gain a Lightning offer: the registrar creates a BOLT 12 offer per name on the node and appends lno= to the DNS record; a settle-forwarder pushes each received payment on to the name's ark destination from a small float wallet (custody window: seconds between Lightning settle and the ark send). Name resolution now runs full client-side DNSSEC validation (src/dnssec.js): binary DoH, root-anchor chain walk, RSA + ECDSA via WebCrypto — the resolver is untrusted transport, verified against live signed zones including the canonical BIP-353 test name. Co-Authored-By: Claude Fable 5 <[email protected]>
* Payment names: ₿[email protected] via BIP-353 DNS payment instructionsAdam Soltys2026-08-016-1/+283
| | | | | | | | | | | | | | | | | | | | Receiving: a Settings card claims a username, signed with the wallet's nostr key; the new names registrar (names/server.js, deployed at names.halwallet.app) enforces first-come ownership by that key and mirrors the claim into a DNSSEC-signed TXT record at <name>.user._bitcoin-payment.halwallet.app whose BIP-21 URI carries the wallet's reusable ark address. The record re-registers itself if the ark address ever changes, and a collapsible hint shows the single CNAME a user adds to serve the same name from their own domain (BIP-353 supports delegation when both zones are DNSSEC-signed). Sending: a pasted name@domain resolves over DNS first — two independent DoH resolvers must agree and both report the DNSSEC AD flag (full client-side chain validation is the stated follow-up) — an ark= instruction pays natively, an on-chain path fills the send form, and a name with no DNS record falls back to the Lightning-address zap flow. Co-Authored-By: Claude Fable 5 <[email protected]>
* Simplify: one Lightning path, one balance, fewer featuresAdam Soltys2026-08-0117-2194/+307
| | | | | | | | | | | | | | | | | | | | | | | | | Dropped: silent payments (feature, wallet chains, scan worker, indexer config), Boltz submarine/reverse swaps (feature + swap.js + tools), the third-party ark swap bridge (config, client flows, settings card — the ASP's own Lightning send is the one path now), and the Consolidate coins settings action. The NWC pouch is gone with them: background answering now spends the WHOLE ark balance. The IndexedDB record mirrors the real manager state plus the chain-3 keys for its live coins and a change-index window; the worker pays through the ASP exactly like the open app, and the app folds the worker's spends, change, and key allocations back in on connect (additive merge + reconcile against the server). A leftover chain-6 pouch from the short-lived pouch era is swept home automatically using the keys stored in its own record. The stated tradeoff: same-origin script access now reaches the ark balance, not a carve-out — the ark balance is walking-around money, the on-chain wallet's keys are never mirrored, and per-connection NWC budgets remain the enforced ceiling. Bundle: 726KB -> 602KB. Co-Authored-By: Claude Fable 5 <[email protected]>
* Default new mainnet wallets to the coinos ASP (ark.coinos.io)Adam Soltys2026-08-011-3/+6
| | | | | | | | Existing wallets with an explicitly chosen provider keep it; a wallet that was riding the old default keeps its second.tech coins under that server's namespace and can reselect it in Settings at any time. Co-Authored-By: Claude Fable 5 <[email protected]>
* Background NWC: the worker now answers — and pays — from the pouchAdam Soltys2026-08-017-23/+552
| | | | | | | | | | | | | | | | | | | | | | | The no-tap half of background wallet-connect. The push payload carries the (encrypted, client-signed) request event; a bundled service-worker responder (src/sw-nwc.js -> nwc-respond.js) finds the matching pouch in IndexedDB, decrypts with the connection's service key, and answers get_info/get_balance/list_transactions from pouch state. pay_invoice pays through the swap bridge with an ArkManager running over the pouch's chain-6 key window (one HTLC lock, one HTTP swap), enforcing per-payment and daily budgets from the pouch's own spend log, checking the notifier's /answered endpoint so it never double-pays or shouts over an open device, and persisting the swap before handing it off so the app can refund a crashed flight. Anything it can't handle falls back to the wake-the-user notification. The notifier grows /publish (verified 23195s only, rate-limited) as the worker's reply pipe and /answered for race avoidance. The app gains pouch funding (mailbox-less arkoor self-send whose recipient bytes we keep), emptying, spend/budget reconciliation on open, and refund recovery with the pouch's own keys. Co-Authored-By: Claude Fable 5 <[email protected]>
* Refresh the push-notifier registration whenever connections changeAdam Soltys2026-08-011-1/+29
| | | | | | | | | | The notifier only wakes a device for service pubkeys it was told about at registration time, so recreating a connection silently killed background wake-ups — the registration kept watching the revoked key forever. Re-register (debounced; the server replaces by endpoint) on init, create, revoke, and sync-merge whenever background alerts are on. Co-Authored-By: Claude Fable 5 <[email protected]>
* Split device sync into per-domain slots with bounded, dirty-only publishesAdam Soltys2026-08-014-18/+89
| | | | | | | | | | | | | | | | 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]>
* Keep sync snapshots under relay size limits, and say when they bounceAdam Soltys2026-08-012-2/+12
| | | | | | | | | | | | 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]>
* Never let a losing device's error shout over a paid zapAdam Soltys2026-08-012-4/+33
| | | | | | | | | | | 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]>
* Add an NWC subscription watchdogAdam Soltys2026-08-011-2/+20
| | | | | | | | | | | The pool's reconnect doesn't heal every socket death (an errored socket sets skipReconnection and gives up), and a wallet service with a dead subscription looks exactly like one that's closed — the client spins with no error, which is what happened to tonight's zaps. Re-subscribe every 90s and on online/visibility transitions; the since window plus the replay guard make the churn and any duplicate deliveries harmless. Co-Authored-By: Claude Fable 5 <[email protected]>
* Reconcile vtxos against the server before a headless NWC paymentAdam Soltys2026-08-011-0/+5
| | | | | | | | | | The sync merge is additive by design, so a vtxo spent on one device can survive in another device's state looking spendable. The interactive send path reconciles on intent; the NWC path connected and spent blind, so a wallet answering from stale state picked an already-spent coin and the ASP refused with 'vtxo … is not spendable (state: spent)'. Co-Authored-By: Claude Fable 5 <[email protected]>
* Make NWC budgets accumulate and travel across devicesAdam Soltys2026-08-011-0/+11
| | | | | | | | | | | | | onRequest handled every request against the connection object captured at listen() time, so each spend was recorded against a stale snapshot — spentToday restarted from the old value on every payment and the daily budget never actually bound. The connection is now re-resolved from stored state per request, which also stops serving a connection revoked on another device mid-session. recordSpend now publishes the sync snapshot so the spend (and the payment's movement + vtxo change) reaches the user's other devices. Test: two payments must accumulate to 52. Co-Authored-By: Claude Fable 5 <[email protected]>
* Default the mainnet ark bridge to coinosAdam Soltys2026-08-011-1/+4
| | | | | | | | | | Small bolt11 sends were unroutable by default: the ASP path's CLN fee budget is a fraction of the ASP's own minimum fee, which rounds to zero sats of routing budget on a zap-sized payment. The coinos bridge now runs open (token gate removed server-side) so the default works for every wallet; users can still pick Off or Custom in settings. Co-Authored-By: Claude Fable 5 <[email protected]>
* Keep relay sockets alive: enable pool ping + auto-reconnectAdam Soltys2026-08-011-1/+5
| | | | | | | | | | | | 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]>
* Surface refused relay publishes; publish NWC info once per sessionAdam Soltys2026-08-012-5/+23
| | | | | | | | | | | | | 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]>
* Pass a single filter to subscribeMany — arrays are rejected by relaysAdam Soltys2026-08-011-2/+6
| | | | | | | | | | 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]>
* Instrument the silent NWC request-drop pathsAdam Soltys2026-08-012-7/+24
| | | | | | | | | 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]>
* Never silently drop an NWC request addressed to usAdam Soltys2026-08-011-1/+30
| | | | | | | | | | | | | | | A request that fails to decrypt was returning without a reply, on the theory that undecryptable traffic wasn't ours. But the event is addressed to this connection's service key and signed by its client key — it IS ours, so a failure there is our bug, and dropping it looks exactly like the wallet being offline. That is why Amethyst spun with no way to tell why. Now it logs the reason, records it for the Settings card, and replies with an INTERNAL error (in both schemes, in case scheme detection was itself wrong) so the client fails fast. The card also shows the last request accepted, so the listener can be seen working without devtools. Co-Authored-By: Claude Fable 5 <[email protected]>
* Wake hal for wallet-connect requests while it is closedAdam Soltys2026-08-013-0/+97
| | | | | | | | | | | | | | | | | | | Step 3, first half. Android Chrome freezes backgrounded PWAs and closes their relay sockets, so a phone with hal "open" answers nothing — which is what made Amethyst spin. Push is the only way to reach a frozen app. - service worker gains a `push` handler. It deliberately does NOT spend: if a window is open it nudges it to re-listen, otherwise it raises a notification that opens hal on tap. Putting a spend path in the worker is the remaining half and wants its own testing. - hal-side: request permission, subscribe with the notifier's VAPID key, and register its service pubkeys with nwcpush.coinos.io. Turning it off deregisters, unsubscribes, and clears the pouch — no reason to leave spendable keys where a worker can read them if nothing will. - a Settings toggle, and the ark feature exposes the ASP context the pouch record needs. Co-Authored-By: Claude Fable 5 <[email protected]>
* Sync NWC connections across devicesAdam Soltys2026-08-011-1/+52
| | | | | | | | | | | | | | | | | | | A connection made on the phone existed only there, so only that device could answer it — and if that device was asleep, a client just spun. Connections now ride the encrypted device-sync snapshot, so whichever device is awake can serve them. The snapshot is nip44-encrypted to a key derived from the seed, the same channel that already carries ark vtxo bytes and gift secrets. The merge is a union by id with two rules that matter: - REVOCATION WINS, so a stale snapshot can never resurrect a revoked connection - the higher same-day spend wins, so a stale device cannot hand a connection back its daily budget Creating or revoking a connection publishes the snapshot immediately rather than waiting for the next state change. Co-Authored-By: Claude Fable 5 <[email protected]>
* Add the NWC pouch: a bounded spending capability for background useAdam Soltys2026-08-011-0/+124
| | | | | | | | | | | | | | | | | | | | | | | | Step 2 of background wallet-connect. Answering while hal is closed means signing in a service worker, and service workers cannot read localStorage where the seed lives — which is a property worth keeping, not working around. So instead of exposing the wallet, carve off a small pouch: - its own key branch (account chain 6), never used for ordinary coins, so a leaked pouch key cannot sign for chain-3 vtxos, the mailbox or LN preimages - only the pouch's private keys and its vtxo bytes go to IndexedDB, which the service worker can read. The seed never does. - it holds only what the user puts in it Threat model, stated in the file rather than implied: same-origin script access or a stolen unlocked device can read IndexedDB and spend the pouch. That is the accepted cost of answering while closed, and it is bounded — the pouch is the blast radius. A user who leaves background NWC off has no pouch. tools/nwc-pouch-test.js asserts the isolation that matters: the pouch branch is distinct from every other branch, exported keys match their pouch pubkeys and cannot control ordinary coins, and no seed material appears in the blob. Co-Authored-By: Claude Fable 5 <[email protected]>
* NWC: own the relay set instead of borrowing the sync feature'sAdam Soltys2026-08-012-10/+33
| | | | | | | | | | | | | | | | | | | | | 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]>
* Add Nostr Wallet Connect (NIP-47) — hal as a wallet serviceAdam Soltys2026-08-016-1/+417
| | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* Stop the nostr sync snapshot from importing another ASP's coinsAdam Soltys2026-08-012-3/+24
| | | | | | | | | | | | | | | | | | | | | The phantom balance came back through the sync path. Snapshots published by older builds carry no ASP tag, so the tag check passed them, and with the local namespace empty the merge returned the snapshot wholesale — writing second.tech's vtxos straight into the coinos namespace, where the manager then stamped them as coinos'. Two fixes, both grounded in the fact that a vtxo names its own cosigner: - the sync path derives the snapshot's true owner from its vtxos and parks it under that owner instead of merging when it isn't provably ours - ArkManager.init drops any state whose coins were cosigned by a different server, whatever the stamp claims. Stamps are something we wrote; the vtxo bytes are evidence. Found by tracing localStorage reads in the live tab: the namespaced key was being *created* during load, which pointed at the merge rather than at storage keying. Co-Authored-By: Claude Fable 5 <[email protected]>
* Repair Ark state that an earlier build stamped with the wrong serverAdam Soltys2026-08-011-1/+36
| | | | | | | | | | | | | | | The first namespacing attempt could brand one ASP's coins with whichever server happened to be connected, filing them under the wrong namespace — a phantom balance that survives both later fixes because the damage is already written to localStorage. loadArkState now checks the stamp against the vtxos themselves. A vtxo names its cosigner in its own bytes, which is fact; the stamp is just something we wrote. When they disagree the state is misfiled: it moves to a holding key under its true owner and this namespace shows nothing. adoptArkState claims from that holding area when the matching server connects. Co-Authored-By: Claude Fable 5 <[email protected]>
* Never render another ASP's Ark state before connectingAdam Soltys2026-08-012-7/+35
| | | | | | | | | | | | | | | | The previous fix still let a phantom balance through: loadArkState fell back to the pre-namespacing key when a namespace was empty, and the UI renders from that before any server is contacted — so the guard, which runs on connect, never got a say. loadArkState now returns only the namespaced state. Legacy state is claimed through adoptArkState(serverPubkey), which proves ownership first: from the state's own stamp, or failing that by decoding a vtxo, whose wire format names its cosigner. No network call, and it cannot guess wrong. On a match the state moves into the namespace and the legacy key is retired; on a mismatch it is left untouched for its real owner. Co-Authored-By: Claude Fable 5 <[email protected]>
* Keep Ark state per ASP, not just per walletAdam Soltys2026-08-012-15/+61
| | | | | | | | | | | | | | | | | | | | | Switching providers showed the previous server's balance and tried to resume its in-flight board ("1 deposit confirming" against a server that had never heard of it). A vtxo is only meaningful against the ASP that cosigned it, so state must be scoped to both the wallet and the server. - ark state keys gain a namespace derived from the ASP endpoint, so switching providers switches state and switching back restores it - state is stamped with the server pubkey; on connect a mismatch is discarded rather than displayed. This also self-corrects the one-time adoption of pre-namespacing state, which can only guess which ASP it belonged to - mergeArkStates refuses to fuse two servers' states, and the nostr sync snapshot carries (and filters on) the ASP it came from, so a device on a different provider can't inject foreign vtxos Nothing is deleted: each ASP's state lives under its own key, and the legacy key is left in place for whichever server rightfully claims it. Co-Authored-By: Claude Fable 5 <[email protected]>
* Settings: coinos ASP presets + swap-bridge providerAdam Soltys2026-07-313-3/+251
| | | | | | | | | | | | | | | - ark presets gain coinos entries (ark.coinos.io on mainnet, ark-staging.coinos.io on mutinynet) alongside Second's - new per-network swap-bridge setting (preset/custom URL + access token) with its own Settings card When a bridge is configured, a bolt11 quote asks it first and shows its fee; paying locks an HTLC only the bridge can claim, and only by revealing the invoice preimage. Any failure — unreachable bridge, refused swap, unpayable invoice — refunds and falls back to the ASP's own lightning-send, so the payment never dead-ends. Hanging swaps are refunded on reconnect. Co-Authored-By: Claude Fable 5 <[email protected]>
* Client support for third-party HTLC vtxos (trustless swap bridges)Adam Soltys2026-07-314-5/+187
| | | | | | | | | | | | | | | | | | Speaks the proposed VtxoPolicy::Htlc (type byte 0x08): policy encode/decode, two-leaf taproot (claimer-with-preimage / refunder-after-expiry), preimage field on the arkoor cosign request, and GetHtlcPreimage. ArkManager gains the swap primitives: htlcLock (lock funds claimable only against a payment hash, refundable by us after expiry), htlcClaim (reveal the preimage), htlcCosignRefund (hand an unclaimable HTLC back — the server constrains the outputs to the refunder, so this is safe to expose publicly) and acceptHtlcRefund. tools/ark/htlc-bridge-test.js drives a full submarine swap between two independent wallets against a patched captaind, and asserts the ASP rejects both a wrong-preimage claim and a preimage-less spend to the bridge itself. Co-Authored-By: Claude Fable 5 <[email protected]>
* Re-render the send form when a destination annotation appearsAdam Soltys2026-07-311-2/+6
| | | | | | | | | The lightning-address zap button is computed at render time, but onInput only re-rendered when on-chain validity flipped — an lnaddr never does, so the button never appeared. Track sendFormNote presence like readiness and re-render on a flip. Verified with a headless-browser check. Co-Authored-By: Claude Fable 5 <[email protected]>
* Fix send-form UX around lightning pays; harden ln state machinesAdam Soltys2026-07-315-55/+119
| | | | | | | | | | | | | | | | | | | - a rejected cosign (gRPC INVALID_ARGUMENT) returns the input vtxo to spendable and fails the action instead of leaving it pending forever — the stuck-pending state that pushed small zaps onto Boltz's 25k minimum - ark ln-pay now takes over when funds are settling (retrying quote) rather than falling through to Boltz - typing a destination clears stale send errors (also cleared on tab switch); paste path likewise - typed lightning addresses no longer auto-advance mid-keystroke (the matcher fired at `[email protected]` before `.com`, yanking the form away): the send form offers an explicit "Zap <addr>" button; pasted/scanned addresses still advance immediately - ln-recv: HTLC vtxos anchored to a not-yet-confirmed pool round park and retry instead of throwing; the requested HTLC expiry is anchored on first prepare so retries can't inflate it past the server's grant Co-Authored-By: Claude Fable 5 <[email protected]>