feat(ecash): Minibits @minibits.cash Lightning address on Cashu receive #156
Closed
ssmithx
wants to merge 9 commits from
feat/minibits-lnurl-receive into main
pull from: feat/minibits-lnurl-receive
merge into: :main
:main
:fix/paid-content-payment-not-lost
:fix/mint-error-detail-swallowed
:investigate/framework-lnd-startup
:fix/minibits-already-redeemed
:cuprate-fixes
:feature/dojobay-app
:fix/bitcoin-core-tor-service-name
:docs/openwrt-gateway-setup
:cuprate-cpu-fix
:fix/cuprate-explicit-logging-levels
:companion/session-2026-08-31
:companion/0.5.28-deploy-handoff
:companion/0.5.28-ship
:companion/0.5.28
:app-bumps-mirror-pending
:cuprate-archyapp
:docs/todo-list
:companion/0.5.27-version-meta
:companion/0.5.27-ship
:companion/0.5.27-clipboard-qr-restart
:ux-at-last
:fix/mesh-send-content-inline-federation-fallback
:cashu-error-messages
:gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf
:feat/podsteadr-app-package
:rotate-release-root
:chore/aiui-monorepo-migration
:wip/phase-13-p13-02
:wip/phase-13-p13-01
:fix/bitcoin-conf-conflict-crash-loop
:demo-build
:archy-hwconfig
:openwrt-enhancements
:a3-10-endpoint-fallback
:public-prelaunch
:release/1.7.115-prep
:fix/web-listener-ipv6
:fips-companion-5g-hardening
:fix/connection-accept-deadlock
:fix/demo-images-path-filter
:fix/companion-autologin-replay-intro
:networking-profits-dashboard
:identities-mobile-polish
:companion-qr-scan-fix
:companion-qr-pairing
:intro-reliability-video-perf
:login-bg-continuity
:audio-bottom-bar
:demo-intro-every-visit
:intro-entrance-fixes
:cloud-feedback-demo-content
:cloud-tabs-search
:ark-wallet-ui-demo
:demo-nginx-app-assets
:demo-ui-fixes
:ark-wallet-barkd
No Reviewers
Labels
Clear labels
companion-agent
queued
roadmap
waiting-external
Companion-app agent's handover list (node-side parts tracked separately)
Triaged, next in this session's queue after current release work
Feature/effort item, deliberately unscheduled — needs scoping + a slot, not a fix today
Waiting on a person outside this session (tester, operator)
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: lfg2025/archy#156
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Adds a
@minibits.cashLightning address (LUD-16) to the ecash Receive tab, derived from and authenticated by the node's existing NUT-13 ecash seed (no second secret to back up). Any Lightning wallet can pay this address; the sats land as ecash.This branch also includes several fixes found and verified live against real payments while getting the feature working end-to-end on
archy-x250-pa3(see commit messages for full detail on each):claim_and_redeemnow persists every fetched claim before attempting decrypt/redeem, so a local failure retries next poll instead of losing the coins. Also self-heals the accepted-mints allow-list so the Minibits mint can never be excluded out from under a claim.archy-x250-pa3truncatedwallet/minibits.jsonto 0 bytes mid-write, which made everywallet.ecash-lnaddresscall hard-fail forever ("Lightning address unavailable").load_statenow treats an empty/corrupt state file the same as a missing one and re-registers (registration is idempotent per pubkey, so it recovers the same address).vue-i18ntreats a bare@as "linked message" syntax, so the address label's literal@minibits.cashcrashed the message compiler the instant the address loaded. Fixed by escaping it as{'@'}(the same pattern the codebase already used elsewhere); found and fixed a second live instance of the same bug (a password-strength validator message) and added a full-locale-sweep test so this class of bug can't ship silently again.POST /claim— the only sourceclaim_and_redeemchecked — never actually returns anything for a real Lightning payment. Confirmed live: Minibits delivers a payment as a NIP-04-encrypted Nostr DM published to relays, not via that REST endpoint. Now fetches fromwss://relay.minibits.cash(+ fallbacks) instead, feeding the existing pending-claims retry pipeline unchanged.CashuToken::deserializetried. Now trims the token string first — a general robustness fix (also protects a hand-pasted token with clipboard whitespace), not just a Minibits workaround.Test plan
cargo test(backend,wallet::— 120 passed) andvitest run(frontend, 1056+ passed), both cleanvue-tsc --noEmitcleanarchy-x250-pa3and verified end-to-end against three real external Lightning payments (20 + 5 + 20 = 45 sats), all redeemed correctly on the first poll after the relay + whitespace fixes landed🤖 Generated with Claude Code
Root cause of "click Receive, click Ecash, the modal disappears" (in both the browser and the Android companion's WebView, since both host the same neode-ui bundle): vue-i18n treats a bare @ as the start of "linked message" syntax. receiveBitcoin.lnAddressLabel ("Your @minibits.cash address:") isn't valid linked-message syntax, so *compiling* that message throws a SyntaxError the instant it's first rendered — i.e. the moment wallet.ecash-lnaddress resolves and the address section becomes visible. The uncaught render-function error blanks the whole teleported modal, which is indistinguishable from it just closing. Confirmed with a real (non-mocked) Vue app + real vue-i18n compiler in a headless Chromium — a Vitest run with `t` mocked to a no-op, which is how the existing component test suite covers this file, cannot catch a bad message string at all. Fixed by escaping the @ as {'@'} — the same pattern the codebase already uses for settings.domainNamePlaceholder ("user{'@'}example.com"). Added a regression test using the real vue-i18n instance instead of the mocked one; verified it fails on the old string and passes on the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EawZPP9iidXj6Tvg3EpG3aSame class of bug as the Minibits address label (settings.passwordNeedSpecial: "...(!@#$%^&* etc.)" — a bare @ vue-i18n parses as linked-message syntax). This one is live in ChangePasswordSection.vue's password-strength validator: typing a new password with no special character throws this exact SyntaxError the moment the message is rendered. Fixed the same way ({'@'} escaping). Added locales/__tests__/i18nMessagesCompile.test.ts, which walks every string in every locale file and asks the real vue-i18n compiler to parse it — confirmed it fails on both bad strings before their fixes and passes clean now, with no other landmines left in either locale file. This closes the whole bug class rather than just these two instances; a future bad interpolation string fails `npm test` instead of only a live crash report. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EawZPP9iidXj6Tvg3EpG3aBranch analysis:
feat/minibits-lnurl-receiveDate: 2026-09-08 · Repo:
/home/debian/archy· Analysis only, no changes made.Scope
main(basedb52c06), plus 2 uncommitted files(
core/archipelago/src/wallet/cashu.rs,core/archipelago/src/wallet/minibits.rs).6effc6b(feature),76d565f(lossless claims),3f52e4c(stateself-heal),
3be6f45/6041eb6/3768395(UI + i18n escapes + sweep test).What the branch does
Gives the node a
@minibits.cashLUD-16 Lightning address derived from thenode's existing ecash seed (NIP-06 Nostr keys at
m/44'/1237'/0'/0/0,seedHash = sha256(bip39 seed)), so restoring the same phrase in the Minibitsapp recovers the same address. Adds RPCs
wallet.ecash-lnaddress(register,idempotent) and
wallet.ecash-lnaddress-claim(redeem arrived payments), and aQR + 8s claim-poll panel on the Cashu receive tab. Later commits harden claims
(persist before redeem, retry queue, accepted-mints self-heal), recover from a
truncated state file, and fix live vue-i18n
@-compile crashes.Verified (ran, without modifying anything)
cargo check -p archipelago: clean (one new deprecation warning from theuncommitted code:
Timestamp::as_u64→as_secs, minibits.rs:574).#[ignore]).ReceiveBitcoinModal.test.ts,.i18n.test.ts,i18nMessagesCompile.test.ts): 5 passed.Filter::pubkey()in the locked nostr 0.44.2 maps to the#ptag query(confirmed in vendored crate source,
filter.rs:587) — so the DM filtercorrectly matches kind-4 events tagged to our pubkey; combined with the
author == server_nostr_pubkeycheck (the correct authorization gate), therelay-DM intake is sound in principle.
lnaddress()andclaim_and_redeem().wallet/minibits.json, holds a bearer JWT) written 0600.seed_hash= sha256(BIP-39 seed)also vector-tested.
web/is gitignored; the local dist rebuild (16:23 today) does containall new
lnAddress*strings — the "grep the built bundle" gate passes.Issues
High — overlapping-claim race (uncommitted diff)
pollLnClaims(ReceiveBitcoinModal.vue:276-279) usessetIntervalevery 8swith no in-flight guard, and neither
wallet.ecash-lnaddress-claimnorecash::receive_tokenholds any lock (grep confirms no Mutex/RwLock/Semaphorein dispatcher or ecash paths). Worst-case single poll duration — auth (2 HTTP
calls) +
/claimPOST (30s client timeout) + 800ms relay-handshake sleep + 10sfetch_events+ redeem loop (mint swaps) — far exceeds 8s. Two concurrentclaim_and_redeemruns then:load_statethe samelast_dm_seen_atbefore either saves,save_state→ watermark rewound and/or pendingtokens duplicated,
pending_claims→ permanent orange "pending retry" banner and endlesspointless retries.
Fix: a backend guard (per-data-dir mutex around
claim_and_redeem) plus anin-flight flag in the UI poll.
Medium — state loss poisons
pending_claimsforeverThe
3f52e4cself-heal treats a corrupt/truncatedminibits.jsonas "noprofile" — correct for the address (re-registration is idempotent per pubkey) —
but it also resets
last_dm_seen_atto 0. Relays never forget: everyhistorical, already-redeemed DM is re-fetched, fails redeem as a double-spend,
and stays in
pending_claimsretrying forever (no drop-after-N-failures path,no dedup of spent tokens).
failed_countcan never return to 0.Medium — non-atomic state writes
save_stateusesfs::write(truncate-then-write). The exact disk-fulltruncation observed on archy-x250-pa3 can therefore destroy
pending_claimstokens that
POST /claimalready consumed server-side (unrecoverable —/claimis once-only). Relay-sourced DMs are re-fetchable,/claim-sourcedones are not. Fix: write-temp-then-rename.
Low — committed code
minibits_error(minibits.rs:199):&body[..body.len().min(180)]panicsif a multi-byte UTF-8 char straddles byte 180 — a panic inside the error
path of an RPC handler. Use
chars().take(180)or floor to a char boundary.register_profile:is_takentest isbody.contains("already")— anon-collision error whose message contains "already" burns all 6 name
attempts. Prefer the structured
error.name == ALREADY_EXISTS.fetch_relay_dmslimit(200)+ watermark jumping to maxcreated_at: ifmore than 200 DMs ever accumulate since the last poll (relays return the
newest 200), the older ones are silently skipped forever. Unlikely, but
silent.
Low — uncommitted diff
fetch_relay_dmswas inserted betweenensure_mint_accepted's doc block and its signature — the"Make sure the Minibits mint is on the accepted-mints allow-list…" paragraph
is now glued onto
fetch_relay_dms, andensure_mint_acceptedhas no doccomment at all.
Timestamp::as_u64(minibits.rs:574) →as_secs.sleep → fetch → shutdown), including public relays
relay.damus.ioandnos.lol. Wasteful, and leaks the derived nostr pubkey's DM traffic to twopublic relays (metadata privacy). Consider a long-lived client or polling
only the service relay first.
Process / design notes
tested and good but uncommitted/unpushed — against the repo's "commit &
push every unit of work" rule. The
cashu.rstrim fix is an independentregression fix (real 2026-09-08 incident: trailing space in Minibits NIP-04
DM content) and deserves its own commit.
third-party service (random name derived from the node seed; mainnet-only,
idempotent). Worth confirming that as a product default.
closed wait until the next open. Safe (relay DMs persist) and consistent
with the "keep this screen open" hint.
origin's URL embeds a plaintext access token in.git/config— consider rotating/scoping it or using a credential helper.Bottom line
Solid, well-documented feature with unusually good regression tests (each fix
carries the incident it fixes). Ship-blocking concerns are the uncommitted
relay-DM channel's concurrency story (overlapping polls racing on state) and
the related pending-claim poisoning; the
minibits_errorslice panic is acheap fix worth doing in the same pass. Everything committed builds green and
all targeted tests pass.
I am implementing fixes for above issues.
Closed at maintainer request pending a fresh review/requirements pass. The branch remains available for reference.
Pull request closed