| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
| |
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
| |
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- 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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- 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]>
|