| Commit message (Collapse) | Author | Age | Files | Lines |
| ... | |
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A service worker cannot hold a relay socket open, so something must tell a
closed browser "a request arrived". This watches the relays for NIP-47
requests addressed to the wallet-service pubkeys a device registers, and
sends that device a Web Push. The service worker will do the actual work in
the browser; keys never leave it.
Scoped to leak as little as possible: the notifier learns the service pubkeys
registered and the timing of requests to them, and nothing else. Request
contents are encrypted to keys it doesn't hold, it never sees the wallet seed,
and it cannot move funds — the worst it can do is fail to wake a device.
No database or accounts: registrations live in one JSON file, expire if a
device stops refreshing, and push endpoints that return 404/410 are dropped.
Wake-ups are de-duplicated across relays and only fire for live traffic, so a
replayed old request can't wake anyone.
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
build.js swaps src/features/index.js for a generated module listing only the
features in its own ALL_FEATURES registry, so registering nwc in index.js
alone was not enough: its i18n strings shipped (they live elsewhere) but its
code never entered the bundle, and the settings card silently never rendered.
- add nwc to build.js ALL_FEATURES, after ark since it drives ark's headless
pay/balance seam
- tools/feature-registry-check.js asserts the two lists agree on membership
AND on construction order, since hook precedence depends on the order
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]>
|
| |
|
|
|
|
|
| |
Binary protobuf POSTs trip signature rules above ~1.5KB; the demo now runs
entirely over public HTTPS with a path-scoped skip in place.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Reference run for the upstream proposal: real deployed ASP, real deployed
bridge, real Lightning settlement — no regtest. Boards a fresh wallet, quotes
a 25 sat zap (1 sat fee vs second's 20 sat mainnet floor), locks the HTLC,
lets the bridge pay and claim, then reads the revealed preimage back out of
the ASP via GetHtlcPreimage.
Board funding goes through a helper on cs because a board output must be
vout 0 and sendtoaddress randomizes output order.
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A third-party swap provider built on the proposed VtxoPolicy::Htlc: users pay
bolt11 invoices from their Ark balance without the ASP being the counterparty
and without trusting the bridge with custody.
Scope is deliberately one-directional. An ASP's lightning *receive* is already
free (0 base / 0 ppm on mainnet); the expensive leg is *send*, with a 20 sat
floor that eats 80% of a 25 sat zap. Only that leg is worth undercutting.
- bridge/server.js: /info, /quote, /swap, /refund over Bun.serve. Validates
the user's HTLC fully (policy, hash, amount, expiry margin, full vtxo chain)
before spending anything on Lightning, pays, then claims with the preimage.
Crash-safe: state is written atomically and every step re-checks reality
(listpays before paying, so a resumed bridge never double-pays).
- bridge/ln.js: minimal CLN backend (unix socket or docker exec), named
params, maxfee-capped pay, pending/paid introspection.
- /refund is safe to expose publicly: the ASP only cosigns a preimage-less
HTLC spend when every output pays the refunder, so it cannot be redirected.
tools/ark/bridge-e2e-test.js drives the real HTTP API against the patched
regtest ASP: a 25 sat zap costs 1 sat, the far-end invoice really settles, the
preimage checks out, resubmission is idempotent, an unroutable invoice reports
refundable rather than done, and the refund lands every sat back.
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The server rejects an arkoor mixing dust and non-dust outputs, which broke
small Lightning payments (a 45-sat HTLC next to change) and tiny plain
sends. Mirror bark's isolation: dust outputs route through a combined
isolation output spent by a fanout tx, splitting a non-dust output to pad
the dust below-threshold. Outputs can therefore split across two vtxos, so
the manager now classifies results by policy instead of index (legacy
single-field actions still resume).
Regtest now runs the full stack: dedicated cln-ark node (secondark/cln-hold,
hold plugin + xpay) wired into captaind with a channel to the coinos dev clc
node. tools/ark/ln-e2e-test.js proves pay (5000 sat and a 25-sat dust zap),
hold-invoice receive with claim, and spending the claimed vtxos.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The ASP is the HTLC counterparty (second.tech's protocol), so no external
swap provider is needed: paying a bolt11 converts pubkey vtxos into
ServerHtlcSend vtxos the server can only claim by revealing the invoice
preimage; receiving grants ServerHtlcRecv vtxos claimed by revealing ours.
Failure paths cosign the HTLC back (revocation) — proven end-to-end against
regtest captaind, which independently derives the HTLC taproots.
- ark/lightning.js: bolt11 decode, lightning fee schedules, all pay/receive
gRPC-web wrappers (cosign, initiate, status, revocation, hold-invoice
receive flow, anti-DoS attestation)
- ark/send.js: arkoor builder generalized to policy outputs (pubkey /
serverHtlcSend / serverHtlcRecv), two-leaf taproot trees, HTLC clause
scripts, reusable per-part cosign helpers for package RPCs
- ark/manager.js: ln-pay and ln-recv crash-safe action state machines
(deterministic receive preimages from the seed, chain 5), mailbox
lightning notifications, unpayable-invoice -> instant revocation
- proto.js: lightning ArkInfo fields + mailbox lightning message decode
- validate/refresh: policy-aware vtxo validation and reconstruction
- features: bolt11 routes via ark when the balance covers it (Boltz stays
the fallback), lightning-invoice receive on the Ark pane
- tools/ark/ln-test.js: harness proving HTLC cosign + revocation against
captaind; manager-test.js updated for the consolidated bc-container chain
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
SP state (lastSpScan watermark + found spUtxos) was in the nostr snapshot
but, unlike Ark/gifts, its cache extension wasn't mergeAlways — so it only
applied via the newest full snapshot at load time, last-writer-wins. The
watermark could regress (a device behind on SP, or mid-rescan, publishing a
newer snapshot would reset another device's watermark), so every device
tended to re-grind the ~1M-tweak backlog independently.
Make it a commutative merge like Ark:
- mergeAlways: true, so it merges live and from older snapshots too.
- lastSpScan takes the MAX (monotonic high-water mark, never regresses) —
this is what makes the block catch-up run once across all devices.
- spUtxos is an additive union by outpoint; a coin we already track is never
mutated, and spend detection stays each device's own job (_refreshSpUtxos
drops backend-spent coins), so stale merged-in coins self-heal on refresh.
- Anti-entropy: republish our superset when we know more, guarded so it stops
once devices converge.
A manual rescan still works — it forces from=1 synchronously (no await before
capturing it), so a concurrent max-merge can't sabotage the re-scan.
Verified with a merge unit test driving the real extension (max/union/converge).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A wallet a few thousand blocks behind gets the whole scan window from the
indexer at once (~1M tweaks over ~3900 mainnet blocks). The catch-up loop
chunked by 100 BLOCKS, but the cost is one EC point-multiply per TWEAK and
mainnet blocks are wildly uneven (up to 2565 tweaks each) — so a 100-block
chunk could be ~25k tweaks, seconds of work that blew the worker's 30s
timeout. On timeout the caller ran the SAME chunk inline on the main thread
while the orphaned worker kept grinding it, pegging both threads and
freezing the visible tab for the whole ~20min catch-up.
Two fixes:
- Chunk by tweak count (budget 2000, always ≥1 block) instead of block
count, so every worker call stays sub-second regardless of block density.
Verified against live mainnet data: max chunk drops from ~25k tweaks to
~2.8k (~850ms, <3% of the timeout).
- On a worker-call timeout, TERMINATE the worker instead of leaving it
grinding the orphaned request behind the caller's inline fallback — that
double-grind was the both-threads-pegged symptom. _ensureSpWorker respawns
a fresh worker on the next call.
Diagnosed via CDP CPU profiling of the live tab (95% self-time in
_spHitBlocks -> _spCandidate -> secp256k1 wNAF/precompute).
Co-Authored-By: Claude Opus 4.8 <[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]>
|
| |
|
|
|
|
|
| |
E2E harnesses (puppeteer + regtest) and perf repros kept locally but
never meant for the tree.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Tag each BOLTZ_PRESET with the network(s) it serves and add
boltzPresetsFor(net) so the provider dropdown only lists backends
valid for the active network — a mainnet wallet no longer shows the
local (regtest) or coinos-staging (mutinynet) backends. getBoltzProviderId()
now only honors a stored choice if it's valid for the current network,
so switching to mainnet can't silently keep using a regtest/mutinynet
backend, and the getBoltzProvider() fallback can no longer resolve to
staging. Also point the testnet default at custom (was the mainnet
Boltz API).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every wallet.emit() re-rendered the whole screen synchronously. On
refocus after the PWA was backgrounded, a burst of emits (WS reconnect,
the wake scan's several emits, syncFromNostr merge, each live device
snapshot) fired a dozen full-screen rebuilds back-to-back, starving
input — navigation lagged ~2s until the burst drained.
Route the subscription through scheduleRender(), which coalesces emits
into at most one render per animation frame. Direct user actions still
call render() synchronously and cancel any pending frame, so a tap or
navigation always renders immediately and takes priority over background
repaints. rAF also skips rendering while hidden and fires once on
refocus.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Fee rates on the electrum backend came from Fulcrum's estimatefee
(Core's estimatesmartfee): historical, laggy, floored at 1 sat/vB, so
it showed 1 across the board while mempool.space showed 11/10. Pull the
configured explorer's live recommendation instead (/v1/fees/recommended,
then /fee-estimates), falling back to estimatefee only when the explorer
is unreachable — self-hosted/regtest behaviour unchanged.
- On mobile a backgrounded page leaves the socket half-open: TCP is dead
but readyState still reads OPEN, so wake() never reconnected and fired
scan RPCs into the void, waiting ~20s for the heartbeat to recover.
wake() now forces a reconnect when the socket has been silent past the
heartbeat window, so a payment shows the instant you refocus.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
layout
- Gifts now sync across devices: gift records + reserved/reclaimed coin
sets ride the encrypted snapshot as a merge-safe extension (like ark),
and every gift state change (create/reclaim/revoke) triggers a
saveCache so it actually publishes. A gift made on one device now
shows on the others.
- Cap the published snapshot at ~40KB JSON (relay rejects >64KB events,
nip44 expands ~1.5x): trim oldest on-chain txs when oversized; another
device backfills via rescan. Typical wallets untouched.
- Service worker: register with updateViaCache:'none' so a new build is
detected immediately (not held by the 4h HTTP cache), and reload once
on controllerchange so the fresh app runs without a manual second
refresh.
- Gift detail: View/Reclaim share a row, Revoke on its own full-width
line so 'Revoke now' stops wrapping.
Co-Authored-By: Claude Fable 5 <[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]>
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Cross-device ark sync only re-published on a state change (send/board),
so a device holding ark funds but making no transaction never wrote its
state to the relay — another device could never see those coins (and a
legacy shared-tag snapshot that got clobbered to zero-ark stayed that
way). connectArk now calls saveCache once on connect when there are
vtxos, publishing this device's balance to its per-device slot so every
device's ark funds reach the relay and get merged, no transaction
needed.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
After a refresh a wallet reverts to watch-only unless the seed was
saved to device — and ark balance/history were gated behind
arkAvailable() (which excludes watch-only), so the on-chain balance
showed but the ark balance vanished. The ark state is persisted
locally and should display read-only exactly like the on-chain
balance. arkStateNow() now reads it whenever Ark is configured for the
network, regardless of watch-only; the Exit link (which needs keys to
act) is hidden for watch-only. Verified: full wallet shows balance +
Exit link, a locked/watch-only wallet shows the balance alone.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The cooperative offboard was buried under the receive-tab board form
and the unilateral exit lived in Settings — both unintuitive. Now a
small 'Exit' link next to the Ark balance opens a dedicated page that
explains and offers both: cooperative (server co-signs, fast, no
on-chain funds needed, small server fee) and unilateral (trustless,
timelocked, pays mining fees). In-flight exit progress (hop counter,
timelock countdown, cancel) moved here from Settings; the receive pane
loses the offboard button and Settings loses the exit button.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
Choosing a wallet used to just load it and show a second screen with a
Claim button. Now tapping a wallet claims immediately (the pre-claim
check auto-fires doClaim), and the claim fee — network fee estimate,
or 'instant, no fee' for ark gifts — shows on the choose screen so
it's still visible before you pick. Fresh-wallet claims keep the
welcome+backup flow.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The balance line and history-row icons read the LIVE ark manager,
which is null until connectArk() finishes its async handshake — so on
refresh the ark balance briefly vanished and ark rows flashed the
Bitcoin icon before flipping to the ark mark. Read-only hooks
(balance, decorateTxRow, historyEntries, detail views) now fall back
to the persisted ark state from storage via arkStateNow(), so they
paint correct on first render; the manager connecting later just keeps
it fresh with identical data (no visible change). Verified: balance
present and board row shows the ark icon from the first 50ms sample
after reload.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Plain on-chain sends/receives fell through to ↓/↑ arrows in the row
circle; now they show the Bitcoin mark like the other rails (ark/sp/
gift). Direction stays legible via the green/red circle tint and the
signed amount. Consistent icon column across every payment type.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
Off-chain Ark rows (arkHistoryItem) showed an arrow + a redundant 'Ark'
text chip; now the Ark mark sits in the direction-colored circle and
the chip is gone — the label + signed amount carry direction. The
detail header gets the badge instead of the chip. And the receive
selector's Bitcoin option uses coinos's real bitcoin.svg (orange disc +
tilted white B) instead of a bare ₿ character.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The receive address-type picker was a native <select>, which can't
render SVG in <option>s. Replace it with a lightweight custom dropdown
(icon-select) that shows a glyph per rail — ₿ Bitcoin, ⚡ Lightning,
the Second mark for Ark, 🕶️ Silent payment — closing on select or an
outside tap. Receive modes gained an icon field; the address option is
built in app.js. Silent-payment receipts (which merge into history as
on-chain-looking rows) now get a rail glyph + SP tag via a
decorateTxRow so they're distinguishable, matching how ark/gift rows
already decorate.
Co-Authored-By: Claude Fable 5 <[email protected]>
|