| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
| |
Claiming into an existing wallet used to drop onto the Receive tab
(address/QR visibly rotating) until the background scan noticed the
credit and fired the celebration. The claim already knows it succeeded
and for how much, so the celebration now shows immediately; the scan
runs behind it and its receive-index advance is acknowledged so the
generic celebration doesn't fire a second time.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Claiming a gift whose funding tx is unknown to the claimer's data
source (different network/chain, or not yet propagated) used to fail
with the misleading "already been claimed". Now:
- the claim screen probes the funding tx up front (esplora's status/
outspend endpoints don't 404 for unknown txids, so outspend alone
can't detect this) and shows a network/data-source notice, and
- a missing-inputs broadcast rejection maps to the same explanation
instead of the generic already-claimed message.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
| |
"Claim it into a brand-new wallet that only you control." only fits the
new-wallet path; claiming into an existing wallet now says which wallet
it lands in ("Claim it into your wallet "Wallet 1".").
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The existing-wallet buttons looked like inert inputs. Now: a "Receive
into one of your wallets:" label, a chevron on each wallet row so it
reads as tappable, and an "or" divider before the new-wallet button.
(tools/ark/gift-shot.js: screenshot harness that mints a real regtest
gift and opens its link.)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
| |
Tap anywhere to proceed instead of a Done button, and the sent amount
shows in red (amount-neg) rather than green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
|
|
| |
grpcStream() parses gRPC-web frames incrementally off fetch's body
stream; the manager keeps a long-lived SubscribeMailbox subscription
(auto-reconnect, 3s backoff) whose messages flow through the same
duplicate-safe processing as the poll. Receives now land the moment the
sender delivers (~sub-second) instead of on the next poll; the poll
stays as fallback and to drive in-flight boards/refreshes.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
|
| |
Ark movements are clickable like on-chain txs. The detail view shows
amount, date, status, the destination address (sends, copyable), the
vtxo id, and for boards the funding txid with copy + explorer link.
Movements now record those fields at completion time; older records
without them still render (amount/date/status only).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
| |
An arkoor send has no mining fee and no coin selection, so pasting an
Ark address now shows just the amount field plus a hint line; the
irrelevant on-chain controls stay hidden.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
| |
Broadcasting a board now lands on a checkmark screen showing the amount
that will arrive (after the board fee), a note about the confirmation,
and the funding txid with copy + explorer link; Done returns to the
Receive tab.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
| |
toast() takes an optional duration; the two informational Ark messages
(full sentences, not confirmations) show for 5s instead of 1.6s.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
|
| |
Selecting Ark on Receive now shows the address plus an inline "move
on-chain funds into Ark" form (with available balance / minimum hints).
The Settings card slims down to server choice, coin count, in-progress
operations, and refresh/consolidate; the balance already shows on the
balance card.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
|
| |
Opening a wallet no longer contacts the ASP unless it already holds
vtxos / history / unfinished operations. Fresh wallets connect on first
use instead: selecting Ark on the Receive tab, confirming an Ark send,
or the Connect button on the Settings card. connectArk() is idempotent
and generation-guarded against wallet switches mid-dial.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
| |
Mainnet defaults to Second (ark.second.tech), regtest to the local
stack. Testnet/mutinynet get their own preset lists (off/custom only —
no public ASP exists there) instead of falling back to mainnet's, and
stay off by default.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
| |
History is the home for Ark movements now; the Settings card keeps
balance, coin count, board, refresh, and in-progress operations.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
| |
Receives, sends, and boards interleave with on-chain transactions in one
time-sorted, paginated timeline, each tagged 'Ark'. Refreshes are
internal churn and stay in the Settings activity list only.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
| |
Same card and tap-to-acknowledge flow as on-chain receives; the ack
persists in the ark state (shows exactly once) and a 2h recency guard
keeps old payments from celebrating when a wallet is opened later.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Settings: Ark server card (off by default, per-network presets incl.
Second mainnet + local regtest, custom URLs) with balance, coin count,
board form (on-chain -> Ark; funding output pinned to vout 0 via the
new buildTx noSort option), refresh/consolidate, and recent activity.
- Receive: Ark option in the address-type dropdown (QR + copy).
- Send: t?ark1 addresses are recognized in the normal send form and
route to an Ark confirmation (no mining fee) with a success view.
- Balance card: Ark balance line under the on-chain figure.
- ArkManager lifecycle: per-wallet instance created on account
activation, background sync every 5s (regtest) / 30s, state persisted
under the wallet cache key.
Verified end-to-end in a real browser (tools/ark/ui-test.js, puppeteer
against the regtest stack): receive, send, board, and refresh all green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Speaks Second's bark/captaind Ark protocol directly from the browser —
gRPC-web over fetch with hand-rolled protobuf, no wasm, no new deps
(crypto via the existing @scure/@noble stack incl. btc-signer's BIP-327
musig2 module).
src/ark:
- proto.js transport, ark address codec, mailbox blinding/auth,
VTXO ProtocolEncoding v2 decode
- send.js arkoor send: checkpoint+arkoor txs, BIP-341 sighashes,
MuSig2 cosign ceremony, signed-VTXO assembly
- board.js onchain -> ark: funding taproot, exit-tx cosign
- refresh.js delegated round (hArk): participation, tx-chain
reconstruction, hash-locked leaf cosign, forfeit swap
- validate.js receive-side verification: chain must land on the vtxo
point, anchor checked onchain, every transition sig verified
- manager.js persistent wallet layer: storage-checkpointed step-machine
actions (send/board/refresh) that survive crashes, mailbox
sync with validation, multi-input consolidation refresh
tools/ark: end-to-end regtest tests (receive, send, board, refresh,
manager incl. crash-mid-send + resume), all passing against captaind
with the bark CLI as counterparty.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
|
| |
|
|
|
|
|
|
|
|
| |
Production Boltz service now live on cs at swap.coinos.io (dedicated mainnet
CLN, chain = coinos production bc on desk over Tailscale). Add it as a
selectable provider. Mainnet default stays on public Boltz Exchange until
our node has channel liquidity.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01RtuRuzDQ6ffngL5wFw4f9v
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A reverse-swap claim is an on-chain receive, so instead of a separate LN
'Received!' card (which stacked with the generic celebration and needed
suppression), credit the claimed coin to the wallet the instant we broadcast the
claim — new wallet.creditReceive() mirrors the instant-payment watcher path
(adds the utxo + pending row, advances the receive index, emits). The generic
'Payment received!' then fires immediately with the right amount, one screen, one
tap. Dropped the LN success card + the suppression logic.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
A reverse-swap claim is also an on-chain receive, so after the LN 'Received!' card
the generic 'Payment received' celebration would fire too — making the first Done
tap appear to do nothing (it just revealed the second screen). Suppress the
generic celebration while the LN claim card is shown, and have its Done ack the
generic one as well, so a single tap dismisses both.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
_txSummary computed a tx's spend only from inputs whose prevout address is on the
scanned frontier (addrMap). A coin spent from an address not currently in that
set — e.g. a fresh reverse-swap claim — was dropped from the total, so the net
showed e.g. -4,768 instead of -50,794, and the scan overwrote applySentTx's
correct net with it. Now: applySentTx counts every input it spent (falling back
to the witness-utxo value if a coin was dropped from the utxo set) and marks the
tx 'local'; the rescan preserves a local tx's net/fee instead of recomputing.
Funds were always correct on-chain — this was a display-only undercount.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A reverse swap's preimage is sha256(swapNode(idx).privateKey) — deterministic per
index. nextSwapIndex was derived only from *saved* swaps, so an attempt that
created the swap at Boltz but threw before saving (e.g. the lockup-address-mismatch
bug) never advanced the index; the next try reused the same preimage and Boltz
rejected it ('a swap with this preimage hash exists already').
Make nextSwapIndex a persisted, monotonic counter (reserves the index on every
call, seeded from saved swaps), and have startReverse retry with the next index
when Boltz reports the preimage already exists — which also recovers wallets
already stuck on a burned index.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
swapP2TR resolves the network via netFor(name) → NETS[name] || btc.NETWORK, but
NETS had no mutinynet entry, so it fell back to MAINNET — Hal computed a bc1p…
swap address while Boltz (bitcoinSignet) returns tb1p…, tripping 'lockup address
mismatch — not paying'. Add mutinynet: btc.TEST_NETWORK (signet/testnet params,
matching the wallet's own netCfg).
Verified: swapP2TR now derives tb1p… for mutinynet (was bc1p…).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Point mutinynet wallets at the new self-hosted staging Boltz backend
(swap-staging.coinos.io) by default, instead of mainnet api.boltz.exchange.
Backend: dedicated boltz CLN + shared mut bitcoind on cs, behind a CORS shim
+ cloudflared.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Send: show only the destination input ('Address or Lightning invoice') until a
valid on-chain/SP address is entered, then reveal amount + fee rate + coin
selection + Review. A pasted bolt11 still auto-advances to its own confirmation,
so those controls never appear for it. New wallet.isOnchainAddress() validator +
a destReady() gate; recipient onInput/paste re-render only when validity flips.
- History: stop listing swaps at the top. Instead, a send/receive that was part of
a swap shows its Lightning context (invoice, swap fee, status, refund) on the tx
detail screen via swaps.findByTxid(). Removed the dead swapListView.
- Lightning receive: drop the '50000' amount placeholder — a bare number looked
like a value was already entered; the min/max hint below covers it.
Verified in-browser: controls hidden until a valid address, then revealed; LN
receive placeholder empty; no errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
The 'Receive over' label was redundant above a self-explanatory dropdown; remove
it (and the now-unused i18n key). Rename the first option to 'Bitcoin address' so
it's clear alongside Lightning and Silent payment.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
The fee label and status copy shouldn't hardcode the provider, since the swap
backend is configurable. 'Boltz fee' → 'Swap fee', and the pay/settle messages no
longer name Boltz. (The settings card still says 'Boltz-compatible provider',
which is an accurate description of the API standard.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- Receive: replace the 3-button segmented control (too wide on mobile) with a
compact 'Receive over' dropdown that scales to more address types.
- Remove the standalone Swap tab. Paying an invoice now lives in Send; the
in-flight/recent swap list folds into the top of History. Deleted the dead
swapTab + doSubmarine.
- Send: as soon as a valid bolt11 is pasted/typed/scanned into the recipient
field, jump straight to the Lightning confirmation (submarine-swap quote with
itemized cost) — the invoice's baked-in amount is used, so the amount field is
skipped entirely. Wired into the recipient onInput, the paste button, and QR scan.
Verified in-browser: Swap tab gone, dropdown renders, and a pasted 60k invoice
auto-progresses to 'Pay over Lightning' with the Boltz fee shown, no Review tap.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The dev server already binds 0.0.0.0, but the app's regtest data source + SP
indexer pointed at localhost, which on a phone means the phone's own localhost.
Proxy them all through the dev server (same-origin), like /boltz already was:
HTTP by prefix (/boltz, /esplora, /sp) and WebSocket by exact path (/electrum →
Fulcrum, /sp/ws → SP indexer), with a Bun websocket relay. The regtest presets
become relative paths, and a small absWsUrl() resolves '/path' WS URLs to
ws(s)://<page-host>/path so they work from any device hitting the dev server.
No per-device config, no CORS. Verified: /electrum relays Fulcrum 2.1.1, /esplora
returns the tip height, and the desktop wallet still loads (balance 98,444) and
connects through the proxy with no errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Paste a bolt11 into the normal Send recipient field and Review now detects it and
routes through a Boltz submarine swap — no 'swap' concept surfaced. Split
startSubmarine into quoteSubmarine (POST + build-but-don't-broadcast the funding
tx) and fundQuotedSubmarine, so the review shows an itemized cost before
committing: Amount + Boltz fee + on-chain network fee = Total. Pay funds the
lockup; Boltz pays the invoice once it confirms.
Verified on regtest: a 60,000-sat invoice quotes to Amount 60,000 / Boltz 362 /
network 442 / Total 60,804, with a Pay button.
Lightning-address (LNURL) input + removing the standalone Swap tab are the
remaining pieces.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Receive now has a segmented [Address | Lightning | Silent payment] toggle.
'Lightning' takes an amount and creates a Boltz reverse-swap bolt11 to share;
when it's paid, Boltz locks up on-chain and the SwapManager claims it into this
wallet automatically (shown live, then 'Received!'). Gated to non-watch-only
wallets (a reverse swap needs our keys). Removed the duplicate 'Receive over
Lightning' card from the Swap tab. Verified in-browser on regtest: real limits
(Min 50,000) load via the proxy and an invoice is created.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
A submarine swap's success is invoice.paid — Boltz has paid the Lightning invoice,
which is the whole point and irreversible. Boltz then claims its on-chain lockup
(transaction.claim.pending → claimed), often deferred to a later batch, so the old
done-set ({transaction.claimed, invoice.settled}) left Hal stuck showing
'paying / waiting for Boltz to pay' indefinitely after the payment had already
gone through. Add invoice.paid + transaction.claim.pending to the done set.
Verified: a live swap stuck at transaction.claim.pending flips to 'success ·
Invoice paid ✓' after the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
to fetch'
The regtest boltz API (localhost:9001) sends no CORS headers, so the browser
blocked cross-origin fetches — creating a Lightning invoice failed with 'failed
to fetch', and the limits GET silently fell back to defaults (min showed 25k
instead of regtest's real 50k). bun E2E tests didn't hit this (no CORS enforced).
dev.js now proxies /boltz/* to localhost:9001 (adding CORS + handling the OPTIONS
preflight), and the 'local' boltz preset points at the same-origin /boltz path.
Public providers (mainnet/testnet) already send CORS and are unaffected.
Verified: GET limits via proxy returns the real 50k/40.29M with CORS; POST
create-reverse succeeds; the browser swap form now shows Min 50,000.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
expected')
A watch-only wallet has no private key, so swapNode().privateKey is null and
startReverse's sha256(node.privateKey) threw 'uint8array expected'. The receive
card wasn't gated by watchOnly (the pay card already was). Hide both for watch-only
and show a prompt to re-enter the recovery phrase, since swaps need the key to
derive the preimage and claim on-chain.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
hardcoded 50k
doReverse rejected anything under a hardcoded 50,000 sats — which happens to be
the regtest provider's minimum, but mainnet boltz's real minimum is 25,000 (and
providers/networks differ), so valid 25k-50k mainnet swaps were refused and the
max was unchecked client-side.
Add SwapManager.reverseLimits() (GET /v2/swap/reverse, cached per API) and use it
in the swap UI: show the real min/max range under the amount field and validate
against it. Reset the cached limits when the network or provider changes.
Verified: regtest min/max 50,000/40,294,967; mainnet 25,000/25,000,000.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |\
| |
| |
| |
| |
| | |
# Conflicts:
# src/api.js
# src/app.js
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The create pane shows the existing draftMnemonic's backup if one is set, only
falling back to the Generate-seed step when it's empty. Nothing cleared the draft
after a wallet was created, and Add wallet jumped to the create tab without
resetting it — so Add wallet presented the already-active wallet's seed, and
'creating' it just re-opened the same account (no new wallet).
Clear draftMnemonic (+ createStep, confirm) when Add wallet starts a fresh
creation, and clear it in openWallet once a seed has been used to create/enter a
wallet, so each new creation generates a fresh seed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Opening a gift link when you already have wallet(s) now shows a 'Choose where to
receive it' screen — each known wallet (full accounts this session plus the
durable directory, deduped by xpub) plus 'Receive into a new wallet'. A
first-timer with no wallets goes straight to a fresh wallet as before.
Claiming into an existing wallet activates it non-provisionally and sweeps the
gift to its receive address (watch-only is fine — re-enter the seed to spend),
skipping the new-seed backup step. Both bearer and locked (NIP-17) gifts route
through the same claimGift() entry. Verified in-browser: selection lists the
wallet + new option + amount; claim-into-address mechanic proven on regtest.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Persist a per-account gift record (shareable link/code + amount, plus a locked
gift's claim code) when a gift is created, so:
- Unclaimed gifts in History get a 'Gift link' button that re-opens the QR + link
(and the claim code for locked gifts), to re-share without recreating the gift.
- Once a gift's coin is spent and we didn't revoke it, it shows in History as
'claimed' instead of vanishing. Revoking marks the record so a reclaim isn't
mislabelled as claimed.
Verified on regtest: create → outstanding=1, link stored; after the recipient
claims → outstanding=0, claimed=1 with the right amount.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| | |
Renamed the default mainnet SP-indexer endpoint to sp.coinos.io. Also switched the
cloudflared bitcoin tunnel to http2 (QUIC was intermittently dropping edge
connections → transient 502s). tweaks.coinos.io still resolves to the same indexer.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| | |
Stop also sending the legacy NIP-04 kind-4 DM — clients that support both (e.g.
Amethyst) were showing the claim code twice. NIP-17 alone covers current clients.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
- Migrated all relay I/O (profile/inbox fetch, DM publish, state sync) to a shared
SimplePool. The old code opened a relay per request and closed it manually,
which raced nostr-tools' CLOSE-after-EOSE and spammed 'WebSocket is already in
CLOSING or CLOSED state'. SimplePool owns the connection lifecycle, so that's
gone. Also reuses connections instead of churning one per fetch.
- Gift DMs now go out as BOTH NIP-04 (kind 4) and NIP-17 (kind 1059 gift wrap).
Older clients (Amethyst) read NIP-04; newer ones (Jumble) only support NIP-17
and ignore NIP-04 — sending both means the claim code shows up either way.
Verified: profile/inbox fetch + DM still work via the pool; the NIP-17 wrap is
published and unwraps to the original message; the NIP-04 kind-4 still lands too.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| | |
card
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
- Settings gains a 'Consolidate coins' card (shown when you hold >1 spendable
coin): wallet.consolidate() does a max self-send merging them into one, so
future sends/gifts use a single input — cheaper and a smaller gift QR.
- At gift creation, detect when the gift would bundle several coins
(giftCoinSummary returns the input count) and trigger the split/consolidate
offer when count ≥ 3 (not just when it frees change). The offer now explains it
plainly ('would bundle N coins, costly to claim') and recommends splitting into
one coin first, with 'Use N coins anyway' as the explicit opt-out.
Verified on regtest: 6 small coins → a 1000-sat gift needs 3 inputs and triggers
the offer; consolidate merges them to 1 coin, after which the same gift is a clean
single input.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
- A many-input gift produces a URL too long for a QR, and qrSvg threw — uncaught,
inside render() — so the click did nothing and showed no error. Wrap the gift QR
in try/catch: render the QR when it fits, otherwise fall back to a 'too large for
a QR — use the link' note. The link itself still works.
- The split-offer card has its own Back; hide the gift sub-view's outer Back while
the split offer is showing, so there aren't two stacked Back buttons.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The gift payload is now encrypted two ways in the link: under the one-time code
(the DM / manual path) AND to the recipient's nostr pubkey via an ephemeral key.
When a NIP-07 browser extension (window.nostr) is present, the claim view offers
'Claim with nostr extension' — it checks the extension's pubkey matches the gift,
decrypts ctKey via window.nostr.nip44.decrypt, and claims into a fresh wallet with
no code. The pasted-code path remains as the fallback for everyone else.
Verified: both decrypt paths recover the payload from the blob; a different
extension account is rejected.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Discover where a recipient reads DMs before sending the claim code: prefer their
NIP-17 DM relay list (kind 10050), else their NIP-65 read relays (kind 10002),
unioned with the broad fallback set (so users without a list still get it). This
makes the locked-gift DM far more likely to land in the recipient's client.
Verified: resolves fiatjaf's real inbox relays; a key with no list returns none
and falls back to defaults.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
A gift can now be locked to a recipient's nostr npub. The shareable link
(/lg/<blob>) carries the amount + recipient in the clear (anyone can see what it
is and who it's for), but the claim payload is encrypted under a one-time code.
That code is delivered as an encrypted nostr DM (NIP-04) to the recipient — so
only they can read it. They paste the code to decrypt, then claim into a fresh
wallet exactly like a normal bearer gift.
- nostr.js: parseNostrPubkey/npubOf, encryptWithCode/decryptWithCode (random
32-byte code), fetchNostrProfile, NostrSync.sendDM (broad relays).
- wallet.js: lockGift → { blob, claimCode }, previewLockedGift, nostr identity +
sendNostrDM.
- app.js: gift form gains an optional 'Lock to npub' field that shows the
recipient's avatar + name (nostr kind-0 profile); create encrypts + DMs the
code (with sent/failed status + a copyable fallback); /lg/ links open a claim
view showing the gift + recipient where you paste the code; account settings
shows your own npub to share.
Verified: code lock/unlock round-trip, a real nostr DM published and read back
from relay.damus.io, and the UI loads + renders with no errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|