Fix CLN channel View Info modal blanking for disconnected channels (#1606)

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>
This commit is contained in:
saubyk 2026-07-17 10:53:02 -07:00 committed by Suheb
parent b790dc7abf
commit c15fa04986
5 changed files with 35 additions and 2 deletions

View file

@ -16,6 +16,17 @@ this release should add its entry under the appropriate section below.
sync, and the list columns read `peer_connected` directly. Regression tests were added for
both channel tables.
- **Core Lightning: fix the channel View Info modal rendering blank for disconnected channels**
([#1625](https://github.com/Ride-The-Lightning/RTL/pull/1625), fixes
[#1606](https://github.com/Ride-The-Lightning/RTL/issues/1606)).
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 showed no value. Because a disconnected channel moves to
the pending/inactive table, this is exactly what was seen on "View Info" for a disconnected
channel. The pending table now passes `selNode` (matching the open table), and the modal guards
the explorer link so a missing `selNode` can no longer blank the dialog.
## Developer Tooling
- **Added a Core Lightning node to the regtest docker fixture**

View file

@ -43,7 +43,7 @@
<h4 fxLayoutAlign="start" class="font-bold-500">Funding Transaction ID</h4>
<span class="foreground-secondary-text">
{{channel.funding_txid}}
<fa-icon matTooltip="{{'Link to ' + selNode.settings.blockExplorerUrl}}" class="ml-1 fa-icon-primary" [icon]="faUpRightFromSquare" (click)="onExplorerClicked()"/>
<fa-icon *ngIf="selNode?.settings?.blockExplorerUrl" matTooltip="{{'Link to ' + selNode?.settings?.blockExplorerUrl}}" class="ml-1 fa-icon-primary" [icon]="faUpRightFromSquare" (click)="onExplorerClicked()"/>
</span>
</div>
</div>

View file

@ -57,6 +57,7 @@ export class CLNChannelInformationComponent implements OnInit {
}
onExplorerClicked() {
if (!this.selNode?.settings?.blockExplorerUrl) { return; }
window.open(this.selNode.settings.blockExplorerUrl + '/tx/' + this.channel.funding_txid, '_blank');
}

View file

@ -1,5 +1,5 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { StoreModule } from '@ngrx/store';
import { Store, StoreModule } from '@ngrx/store';
import { RootReducer } from '../../../../../store/rtl.reducers';
import { LNDReducer } from '../../../../../lnd/store/lnd.reducers';
@ -73,6 +73,20 @@ describe('CLNChannelPendingTableComponent', () => {
expect(connectedCellText()).toBe('Disconnected');
});
// Issue #1606: the channel information modal reads selNode.settings.blockExplorerUrl, so the
// pending table must pass selNode when opening it. Without it the modal throws mid-render and
// blanks State/Connected/balances for disconnected channels (which live in this table).
it('should pass selNode when opening the channel information modal', () => {
const store = TestBed.inject(Store);
const dispatchSpy = spyOn(store, 'dispatch');
const selNode: any = { settings: { blockExplorerUrl: 'https://mempool.space' } };
component.selNode = selNode;
component.onChannelClick({ short_channel_id: '120x1x0', peer_connected: false } as any, {} as any);
expect(dispatchSpy).toHaveBeenCalled();
const action: any = dispatchSpy.calls.mostRecent().args[0];
expect(action.payload.data.selNode).toBe(selNode);
});
afterEach(() => {
TestBed.resetTestingModule();
});

View file

@ -20,6 +20,7 @@ import { openAlert, openConfirmation } from '../../../../../store/rtl.actions';
import { RTLState } from '../../../../../store/rtl.state';
import { closeChannel } from '../../../../store/cln.actions';
import { channels, clnPageSettings, nodeInfoAndBalanceAndNumPeers } from '../../../../store/cln.selector';
import { rootSelectedNode } from '../../../../../store/rtl.selector';
import { ColumnDefinition, PageSettings, TableSetting } from '../../../../../shared/models/pageSettings';
import { CamelCaseWithReplacePipe } from '../../../../../shared/pipes/app.pipe';
import { MAT_SELECT_CONFIG } from '@angular/material/select';
@ -62,6 +63,7 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
public errorMessage = '';
public apiCallStatus: ApiCallStatusPayload | null = null;
public apiCallStatusEnum = APICallStatusEnum;
public selNode: Node | null = null;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<RTLState>, private rtlEffects: RTLEffects, private commonService: CommonService, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
@ -109,6 +111,10 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
}
this.logger.info(channelsSeletor);
});
this.store.select(rootSelectedNode).pipe(takeUntil(this.unSubs[4])).
subscribe((nodeSettings) => {
this.selNode = nodeSettings;
});
}
ngAfterViewInit() {
@ -133,6 +139,7 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
payload: {
data: {
channel: selChannel,
selNode: this.selNode,
showCopy: true,
component: CLNChannelInformationComponent
}