LND's DELETE /v1/channels/{channelPoint} streams until the closing tx
confirms, routinely longer than the wrapper's 10-minute bound. The
close call in closeChannel is fire-and-forget (202 returned
immediately, no .catch), so the timeout rejection would have become an
unhandled promise rejection and crashed the process ~10 minutes after
any close that had not yet confirmed. request-promise returned
Bluebird promises whose unhandled rejections only warned, which is why
this never crashed before.
The close now uses a copy of the options with timeout: 0 (same
treatment as the invoice/payment subscriptions) and a .catch that logs
through handleError - errors were never surfaced to the HTTP response
anyway, but logging beats Bluebird's silent warning. This was the only
call site without a rejection handler.
Verified on the fixture: opened a disposable 200k alice->bob channel
via RTL, closed it (202, gone from open and listed in closed after
mining), then requested a close for a bogus channel point - LND
rejects the stream, the catch logs the error (no auth headers in it),
and the process stays up. Read suite re-passes.
The 10-minute timeout added for review feedback would have aborted
LND's long-poll subscription streams (/v2/invoices/subscribe and
/v2/router/track), which legitimately stay open until an invoice
settles or a payment resolves - breaking real-time notifications for
any invoice paid more than 10 minutes after creation.
The wrapper now honors a per-call options.timeout (0 disables the
bound, axios semantics; the 10-minute default still applies everywhere
else), and both subscription calls pass timeout: 0. They also copy the
options object instead of mutating it: addInvoice hands the
session-cached options to subscribeToInvoice, so setting the timeout
in place would have leaked an unbounded timeout to every subsequent
request for that node (getOptions resets form/body/qs but not
timeout).
Verified on the regtest fixture: with a websocket client connected as
alice's frontend, creating an invoice opens the subscription stream,
it survives idle, and paying it from the CLN node delivers the SETTLED
event over the websocket in real time. The per-call override was also
verified directly (timeout: 1000 aborts a slow upstream with
ECONNABORTED; timeout: 0 waits it out). Both API suites (31 read + 12
write checks) re-pass.
Review feedback on #1638 flagged two issues in the wrapper:
1. Array form values (eclair's ignoreNodeIds on findroutebetweennodes)
encoded as "ignoreNodeIds=a,b" via String(), and worse, an empty
array produced "ignoreNodeIds=" which Eclair's pubkey list parser
rejects - breaking the default findroute path that worked under
request-promise (qs omitted empty arrays). Arrays are now omitted
when empty and comma-joined when not, matching Eclair's CsvSeq list
format. Verified against the fixture: route eclair->bob->carol is
found with an empty ignore list and disappears when bob is ignored.
Under request-promise's qs indexed encoding (ignoreNodeIds[0]=...)
Eclair never matched the field name, so the ignore list was silently
dropped; this change makes it effective for the first time.
2. The shared transport had no request timeout, so a hung upstream
held connections open indefinitely. Added a 10-minute bound,
sized to the slowest legitimate operations (LND's /v2/router/send
streams up to timeout_seconds=600; slow CLN channel operations get
req.setTimeout(600000) upstream).
Both API suites (31 read + 12 write checks) re-pass on the fixture.
request has been deprecated since 2020 with an unfixed SSRF advisory and
pins vulnerable copies of form-data (critical), qs, tough-cookie and
uuid - 8 of the 13 remaining production audit findings, none fixable by
version bumps (issue #1634, item 1).
All 36 backend files that imported request-promise now use a small
compatibility wrapper (server/utils/request.ts) backed by axios, which
is already a production dependency. The wrapper accepts the existing
options shape (qs, form - object or pre-encoded string, body,
baseUrl/uri, rejectUnauthorized, json), resolves with the response body
directly, and rejects with a plain object mirroring request-promise's
StatusCodeError/RequestError shape, so CommonService.handleError works
unchanged (ECONNREFUSED -> 503, Eclair StatusCodeError -> 500, nested
error body extraction). Auth headers are excluded from rejected errors
so they cannot leak into logs. Callers without json: true (block
explorer, currency rates) still get raw text bodies, and LND's
line-delimited /v2/router/send stream still surfaces as a string for
the existing parser.
Only behavioral code change: CLN verifyMessage used request-promise's
callback style and was ported to the same promise style as signMessage;
four Eclair handlers gained explicit returns to satisfy
noImplicitReturns once the import became typed.
Production npm audit drops from 13 findings (2 critical) to 6 low, all
in the crypto-browserify/elliptic chain tracked in #1634.
Verified against the docker regtest fixture with 43 API checks across
LND, Core Lightning and Eclair: reads, invoice creation, a routed LND
payment over the streaming endpoint, cross-implementation payments from
CLN and Eclair, message sign/verify, channel backup to disk, and
bad-invoice/node-unreachable error mapping. Lint and both production
builds are clean.
Per LND v0.21.0 release notes, the sat_per_byte option will be removed
in v0.22 across CloseChannel, OpenChannel, SendCoins, SendMany, and
walletrpc.BumpFee. LND already treats sat_per_byte as sat/vbyte
internally, so this is a pure rename with no value conversion. Updates
both the wire-format strings sent to LND and the matching TypeScript
identifiers across the close-channel, open-channel, send-coins, and
bump-fee paths.
The a11y fix in #1609 wrapped the bare Scroll Range mat-select in a
mat-form-field for its label, but the wrapper reserved subscript space
(78.8px vs the date field 56px) and anchored to the row top, leaving
the date picker ~11px lower on every implementation reports screen.
Use subscriptSizing="dynamic" (no hints are used) and center on the
cross axis, restoring the aligned 56px row from v0.15.8 while keeping
the label. Verified headlessly against the regtest fixture: both
fields now render at identical top/height.
Fixes#1635
The fix is within the pinned 0.3.x line but the exact pin kept npm
update from reaching it. Clears the last high-severity production
vulnerability; frontend build and full spec suite verified.
Apply the bumps from all 20 open Dependabot security PRs (#1583-#1617)
in one pass on the release branch: axios 1.16.0, ws 8.21.0, the
socket.io server stack, express path-to-regexp, follow-redirects,
lodash and the remaining flagged transitive deps. Angular framework
packages move in lockstep to 20.3.26 and the CLI/build toolchain to
20.3.32, which drops the vulnerable node-forge from the tree entirely.
Also pick up in-range fixes without open PRs (qs, uuid, tough-cookie,
cookie, ajv, bn.js, elliptic, socket.io-parser).
npm audit: 85 vulnerabilities (23 prod) -> 30 (14 prod). The remainder
(request/request-promise, csurf, pdfmake, crypto-browserify chain)
needs code changes, not bumps, and is tracked separately.
Verified: lint, 199 frontend specs, backend + frontend production
builds, and an end-to-end smoke test against the docker regtest
fixture (LND, CLN and Eclair auth/getinfo/channels + WS upgrade).
Dependabot PRs target master and are resolved in batch PRs against the
release branch, but the process was undocumented. Add a "Handling
Dependabot PRs" section covering target collection, pinned in-range
fixes, Angular lockstep, from-scratch lockfile regeneration, artifact
rebuild, verification, and issue-tracking for deprecated packages.
Mermaid flowchart (rendered by GitHub) showing the channel graph, each
node's chain-backend link -- including eclair's dedicated wallet and
hashblock ZMQ endpoint -- and the protocol RTL uses to reach each node.
Completes backend coverage of RTL's three implementations in the docker/
dev fixture: an eclair node (polarlightning/eclair 0.13.1) joins the three
LND nodes and the CLN node, wired to RTL over its HTTP API with basic auth,
and the seed opens an eclair->bob channel (3.5M sats, 1M pushed), sends two
direct payments and leaves one open invoice.
Non-obvious plumbing this needed:
- polarlightning/eclair instead of acinq/eclair: the official image is
amd64-only (useless on Apple Silicon) and its newest versioned tag is
years stale; Polar builds the same ACINQ source multi-arch.
- Eclair has no on-chain wallet of its own -- it drives a bitcoind wallet.
A new eclair-wallet-init container creates a dedicated "eclair" wallet
before the node starts; without it eclair attaches to "the default
loaded wallet", i.e. the rtldev mining wallet.
- bitcoind now also publishes a zmqpubhashblock endpoint (28336): eclair's
bitcoind.zmqblock consumes the hashblock topic, not the rawblock one LND
uses. Wired to rawblock, eclair never sees new blocks and channels hang
in WAIT_FOR_FUNDING_CONFIRMED.
- Eclair confirms channels at 8 blocks (channel.min-depth-blocks), not 6,
and 'open' returns before the funding tx is broadcast -- the seed waits
for the mempool and mines 8 blocks for this channel.
Adds a bin/e-cli helper (eclair-cli with the API password), updates the
README, and verified end-to-end: seed completes, the channel reaches
NORMAL, both payments settle, and RTL's /rtl/api/ecl endpoints return the
node, channel and invoice data.
The #1606 fix added 'channel.connected = !!channel.peer_connected' to
server/controllers/cln/channels.ts but the committed compiled artifact
backend/controllers/cln/channels.js was never regenerated, leaving it stale.
Rebuild it so the committed backend output matches its TypeScript source.
Follow-up to the second #1629 review:
- F4: the limiter invokes its done callback outside the surrounding .then/.catch,
so a throw in the response-send body became an unhandled rejection with no
response (a 500 -> hang regression, notably on LND postPeer where the inner
.catch was removed). Wrap each converted done body in try/catch that sends the
error response, guarded by res.headersSent.
- F5: CLN postPeer re-listed peers but never resolved their aliases, so a freshly
connected CLN peer came back with a raw node id (the frontend uses this response
directly). Resolve aliases through the same bounded limiter, matching LND postPeer.
- F6: make runWithConcurrencyLimit fire 'done' exactly once via a one-shot guard,
so multiple synchronous completions (e.g. non-function task elements) can't
double-send the response.
Follow-up to the #1501 review (PR #1629):
- F1: CLN getAlias now builds its request from selNode.authentication.options
instead of the shared module-level 'options'. That coupling meant a cold
Peers/route lookup dereferenced a null 'options'; with the new limiter
swallowing per-task throws, that returned 200 with every alias unset. Aliases
now resolve regardless of call order, with a truncated-id fallback if auth
options are somehow absent.
- F2: mirror the 20-way concurrency bound to LND peers (getPeers and postPeer),
which had the same unbounded Promise.all alias fan-out. Eclair resolves
aliases inline from a bulk nodes list, so it needs no change.
- F3: normalize runWithConcurrencyLimit's start count to at least 1 so a
non-positive limit can't leave 'done' unfired and hang the response.
RTL resolves peer aliases by calling listnodes once per peer. A prior fix
(1cec7b1) bounded this to 20 concurrent calls plus a cache for the channel
list, but the peers list and route lookup still used an unbounded Promise.all,
firing one request per peer at once. On nodes with many peers this overwhelms
clnrest and fails with 'Resource temporarily unavailable (os error 11)'
(EAGAIN), so aliases fall back to raw node IDs.
- peers.ts and network.ts getRoute now resolve aliases via
runWithConcurrencyLimit(tasks, 20, ...), matching the channel list.
- Harden runWithConcurrencyLimit to call done() immediately for an empty task
list; otherwise an empty peers/route set would never send a response.
- Give the alias cache a 6h TTL and a max size (evicting oldest) so aliases
refresh without an RTL restart and the cache can't grow unbounded.
The accessibility edits dropped the final newline from six form templates.
Add it back so these files end with a newline again (POSIX text-file
convention; keeps diffs clean and avoids no-newline lint noise).
Several PRs were merged onto Release-0.15.9 before the per-PR release-notes
process was established, leaving them undocumented. Add entries for:
- #1581 Fix page-load error when a channel alias is undefined (Bug Fixes)
- #1601 Fix stale auth options blocking a not-yet-ready node (Bug Fixes)
- #1582 Add Disable Authentication option (Enhancements)
- #1621 Rebuild the regtest docker fixture (Developer Tooling)
Blocks-till-maturity is critical information for a force-closing channel but
was only visible in the per-channel detail modal. The column and its data
binding already existed in the pending force-closing table (and was selectable
via column settings); it was just missing from the default column selection.
Add blocks_til_maturity to the pending_force_closing default columnSelection
and columnSelectionSM so it is surfaced on the list by default on both desktop
and mobile.
A dependency-update commit in the 0.15.8-beta cycle mechanically renamed
the paginator binding [showFirstLastButtons] to [hidePageSize] on every
mat-paginator while keeping the same 'screenSize === XS ? false : true'
expression. The two properties have opposite polarity, so this inverted
the behavior: on desktop the page-size selector was hidden (locking users
to 10 items per page) and the first/last-page buttons were dropped as
collateral. Revert the ~44 affected paginators back to [showFirstLastButtons]
across the LND, CLN, Eclair and shared tables.
Address review F7/F8 on #1625:
F7 (verification): the onchain.ts `connected === false` branch reads /v1/listfunds
(CLN's own connected field) and only buckets balance as inactive — it is not the
listPeerChannels mirror and does no close logic, so the coercion activates nothing
there. Reword the mirror comment, which inaccurately implied onchain.ts consumes it;
the mirror simply keeps the documented backward-compat `connected` field defined.
F8: hardcode the rune path in create-rune.sh to /root/.lightning/rtl.rune so it
matches the volume mount, healthcheck and RTL runePath instead of deriving it from
${LIGHTNINGD_DATA}, removing the silent-divergence risk.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review F6 on #1625: the LND channel information modal has the same
unguarded selNode.settings.blockExplorerUrl binding as the CLN one, and it is
opened without selNode from the active-HTLCs and channel-backup tables, so it can
blank out the same way. Guard the explorer link (*ngIf + a no-op click when the
url is absent) so a missing selNode can no longer blank the dialog. Eclair's modal
doesn't use selNode.settings, so it needs no change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review F5 on #1625: create-rune.sh was a one-shot poststart script — if
the RPC wasn't ready within its poll or createrune failed, it exited without ever
writing rtl.rune, and since the cln healthcheck gates on that file and rtl waits
on service_healthy, a failed pass deadlocked the whole stack until 'down -v'.
Drive rune creation from the healthcheck instead: the script is now a quick,
idempotent single attempt, and the healthcheck runs it on every interval, so a
transient RPC-startup race just retries and self-heals. Moved the script out of
lightning-poststart.d to /opt and updated the healthcheck, compose comment and
README accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review F4 on #1625: the new selNode field resolved to the global DOM
Node type because the RTL Node model was not imported. Import Node from
shared/models/RTLconfig so the field, the rootSelectedNode store value, and the
CLNChannelInformationComponent it feeds all agree, restoring type-checking.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The channel information modal renders a block-explorer link from
selNode.settings.blockExplorerUrl, but the pending/inactive channels table
opened the modal without passing selNode. With it undefined, that binding threw
during change detection and blanked every field below it — State, Connected,
Private and the balances all rendered without a value. A disconnected channel
moves to the pending/inactive table, so this is what surfaced on View Info for a
disconnected channel (the symptom in the original report).
Pass selNode from the pending table (matching the open table), and guard the
modal's explorer link (*ngIf + a no-op click when the url is absent) so a missing
selNode can no longer blank the whole dialog. Add a regression test asserting the
pending table passes selNode when opening the modal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fixture had only bitcoind + three LND nodes, so RTL's Core Lightning screens
had no backend to exercise. Add a `cln` node (official elementsproject/lightningd
image, multi-arch) wired to RTL over clnrest with rune auth, and have the seed
open a cln->alice channel so the CLN channel/peer screens have real data.
- docker-compose.yml: cln service (clnrest on 0.0.0.0:3010, https), a healthcheck
gated on the rune file so rtl waits for it, and rtl now mounts the cln volume
read-only and depends on cln being healthy. The rtl image is parameterized via
${RTL_IMAGE:-...} so an unreleased build can be tested against the fixture.
- cln/poststart.d/create-rune.sh: once the RPC is up, create a master rune and
write it as LIGHTNING_RUNE="..." where RTL reads it (runePath). Polls for RPC
readiness because the image entrypoint can invoke poststart before the socket
exists.
- RTL-Config.regtest.json: add node index 4 (CLN, rune auth, https://cln:3010).
- seed.sh: fund cln, connect to alice, open a 4,000,000 sat channel, wait active.
- README + release notes updated.
Used to verify the CLN channel connection-status fix (#1606) end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review F3 on #1625: the Close-Channel *ngIf still read the legacy
`connected` field while its neighboring column now reads peer_connected. Point
it at peer_connected directly so it no longer depends on the backend mirror,
removing the latent coupling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback on #1625: copy peer_connected onto the legacy
`connected` field as a real boolean (!!), so strict-equality readers such as
onchain.ts's `connected === false` behave correctly when peer_connected is
absent, instead of leaving `connected` undefined.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a release-notes/ folder to collect the changes for each release, and
add Release-notes-0.15.9.md with the entry for the CLN channel connection status
fix (#1606 / #1625). New process: every PR for a release appends its entry to the
respective release notes document.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLN's listpeerchannels reports connection state as peer_connected, but the
open/pending channel list columns read the legacy `connected` field, which the
backend never populated. It was therefore always empty, so the list always
rendered "Disconnected" while the detail panel (which reads peer_connected)
showed the true state — the contradiction reported in #1606.
Normalize `connected = peer_connected` in the backend listPeerChannels response
so legacy consumers stay in sync, and point the list columns at peer_connected
directly. Add regression specs asserting the connected column follows
peer_connected even when the legacy field disagrees.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>