chore: sidebar events

This commit is contained in:
Anthony Potdevin 2026-03-13 23:19:25 -06:00
parent fd3387ae4e
commit fdcd6dafaa
No known key found for this signature in database
GPG key ID: 4403F1DFBE779457
8 changed files with 608 additions and 154 deletions

View file

@ -10,6 +10,7 @@ import { Toaster } from 'react-hot-toast';
import { TooltipProvider } from './components/ui/tooltip';
import { useListener } from './hooks/UseListener';
import { SseProvider } from './context/SseContext';
import { EventLogProvider } from './context/EventLogContext';
import { config } from './config/thunderhubConfig';
import { LoadingCard } from './components/loading/LoadingCard';
import { useGetNodeInfoQuery } from './graphql/queries/__generated__/getNodeInfo.generated';
@ -162,11 +163,13 @@ export default function App() {
<ConfigProvider initialConfig={{ theme: savedTheme }}>
<SseProvider>
<ContextProvider>
<TooltipProvider>
<Wrapper>
<AuthenticatedRoutes />
</Wrapper>
</TooltipProvider>
<EventLogProvider>
<TooltipProvider>
<Wrapper>
<AuthenticatedRoutes />
</Wrapper>
</TooltipProvider>
</EventLogProvider>
</ContextProvider>
</SseProvider>
</ConfigProvider>

View file

@ -0,0 +1,123 @@
import {
FC,
ReactNode,
createContext,
useContext,
useReducer,
useCallback,
} from 'react';
const MAX_ENTRIES = 100;
export type EventField = { label: string; value: ReactNode };
export type EventLogEntry = {
id: string;
type: string;
title: string;
summary: EventField[];
details: EventField[];
timestamp: number;
status: 'success' | 'error';
};
type State = {
entries: EventLogEntry[];
};
type Action =
| { type: 'add'; entry: Omit<EventLogEntry, 'id' | 'timestamp'> }
| { type: 'dismiss'; id: string }
| { type: 'clear' };
type Dispatch = (action: Action) => void;
const StateContext = createContext<State | undefined>(undefined);
const DispatchContext = createContext<Dispatch | undefined>(undefined);
let nextId = 0;
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'add': {
const entry: EventLogEntry = {
...action.entry,
id: String(++nextId),
timestamp: Date.now(),
};
return {
entries: [entry, ...state.entries].slice(0, MAX_ENTRIES),
};
}
case 'dismiss':
return {
entries: state.entries.filter(e => e.id !== action.id),
};
case 'clear':
return { entries: [] };
default:
return state;
}
};
export const EventLogProvider: FC<{ children?: ReactNode }> = ({
children,
}) => {
const [state, dispatch] = useReducer(reducer, { entries: [] });
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
export const useEventLogState = () => {
const context = useContext(StateContext);
if (context === undefined) {
throw new Error('useEventLogState must be used within an EventLogProvider');
}
return context;
};
export const useEventLogDispatch = () => {
const context = useContext(DispatchContext);
if (context === undefined) {
throw new Error(
'useEventLogDispatch must be used within an EventLogProvider'
);
}
return context;
};
export const useEventLog = () => {
const dispatch = useEventLogDispatch();
const addEvent = useCallback(
(event: {
title: string;
summary: EventField[];
details?: EventField[];
status?: 'success' | 'error';
type?: string;
}) => {
dispatch({
type: 'add',
entry: {
title: event.title,
summary: event.summary,
details: event.details ?? [],
status: event.status ?? 'success',
type: event.type ?? 'event',
},
});
},
[dispatch]
);
const clearEvents = useCallback(() => {
dispatch({ type: 'clear' });
}, [dispatch]);
return { addEvent, clearEvents };
};

View file

@ -1,12 +1,8 @@
import { useApolloClient } from '@apollo/client';
import { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import toast from 'react-hot-toast';
import {
getNodeLink,
getTransactionLink,
renderLine,
} from '../components/generic/helpers';
import { FC, useCallback, useEffect, useRef } from 'react';
import { getNodeLink, getTransactionLink } from '../components/generic/helpers';
import { useNotificationState } from '../context/NotificationContext';
import { useEventLog, EventField } from '../context/EventLogContext';
import { formatSats } from '../utils/helpers';
import { useChannelInfo } from './UseChannelInfo';
import { useNodeDetails } from './UseNodeDetails';
@ -14,19 +10,6 @@ import { useSse, useSseEvent } from './useSse';
const refetchTimeMs = 1000 * 1;
const renderToast = (
title: string,
content: JSX.Element | null | string | number
) => {
return (
<div>
{title}
<div className="h-1" />
{content}
</div>
);
};
const PeerAlias: FC<{ pubkey: string }> = ({ pubkey }) => {
const { alias } = useNodeDetails(pubkey);
@ -42,12 +25,10 @@ const ChannelPeerAlias: FC<{ id: string }> = ({ id }) => {
};
export const useListener = (disabled?: boolean) => {
const { channels, forwardAttempts, forwards, invoices, payments, autoClose } =
const { channels, forwardAttempts, forwards, invoices, payments } =
useNotificationState();
const options: { duration?: number } = useMemo(() => {
return autoClose ? {} : { duration: Infinity };
}, [autoClose]);
const { addEvent } = useEventLog();
const refetchQueryTimeout: { current: ReturnType<typeof setTimeout> | null } =
useRef(null);
@ -87,33 +68,27 @@ export const useListener = (disabled?: boolean) => {
message;
if (is_confirmed) {
toast.success(
renderToast(
'Invoice Paid',
<>
{renderLine('Description', description)}
{renderLine('Description Hash', description_hash)}
{renderLine('Amount', formatSats(received))}
</>
),
options
);
addEvent({
title: 'Invoice Paid',
summary: [{ label: 'Amount', value: formatSats(received) }],
details: [
{ label: 'Description', value: description },
{ label: 'Description Hash', value: description_hash },
],
});
} else {
toast.success(
renderToast(
'New Invoice Created',
<>
{renderLine('Description', description)}
{renderLine('Description Hash', description_hash)}
{renderLine('Amount', formatSats(tokens))}
</>
),
options
);
addEvent({
title: 'New Invoice Created',
summary: [{ label: 'Amount', value: formatSats(tokens) }],
details: [
{ label: 'Description', value: description },
{ label: 'Description Hash', value: description_hash },
],
});
}
handleRefetchQueries(['GetInvoices']);
},
[handleRefetchQueries, invoices, options]
[handleRefetchQueries, invoices, addEvent]
);
const handlePayment = useCallback(
@ -122,25 +97,25 @@ export const useListener = (disabled?: boolean) => {
const { hops, fee, destination, tokens } = message;
const hopLines = hops.map((h: any, index: number) =>
renderLine(`Hop ${index + 1}`, h.channel)
);
const hopFields: EventField[] = hops.map((h: any, index: number) => ({
label: `Hop ${index + 1}`,
value: h.channel,
}));
toast.success(
renderToast(
'New Payment',
<>
{renderLine('Destination', <PeerAlias pubkey={destination} />)}
{renderLine('Amount', formatSats(tokens))}
{renderLine('Fee', fee ? formatSats(fee) : null)}
{hopLines}
</>
),
options
);
addEvent({
title: 'New Payment',
summary: [
{ label: 'Amount', value: formatSats(tokens) },
{ label: 'Destination', value: <PeerAlias pubkey={destination} /> },
],
details: [
...(fee ? [{ label: 'Fee', value: formatSats(fee) }] : []),
...hopFields,
],
});
handleRefetchQueries(['GetPayments']);
},
[handleRefetchQueries, payments, options]
[handleRefetchQueries, payments, addEvent]
);
const handleForward = useCallback(
@ -158,40 +133,35 @@ export const useListener = (disabled?: boolean) => {
if (is_send || is_receive) return;
if (!is_confirmed && forwardAttempts) {
toast.error(
renderToast(
'Forward Attempt',
<>
{renderLine('In Peer', <ChannelPeerAlias id={in_channel} />)}
{renderLine('Out Peer', <ChannelPeerAlias id={out_channel} />)}
{renderLine('In Channel', in_channel)}
{renderLine('Out Channel', out_channel)}
{renderLine('Tokens', formatSats(tokens))}
{renderLine('Fee', fee ? formatSats(fee) : null)}
</>
),
options
);
addEvent({
title: 'Forward Attempt',
summary: [{ label: 'Tokens', value: formatSats(tokens) }],
details: [
{ label: 'In Peer', value: <ChannelPeerAlias id={in_channel} /> },
{ label: 'Out Peer', value: <ChannelPeerAlias id={out_channel} /> },
{ label: 'In Channel', value: in_channel },
{ label: 'Out Channel', value: out_channel },
...(fee ? [{ label: 'Fee', value: formatSats(fee) }] : []),
],
status: 'error',
});
}
if (is_confirmed && forwards) {
toast.success(
renderToast(
'Successful Forward',
<>
{renderLine('In Peer', <ChannelPeerAlias id={in_channel} />)}
{renderLine('Out Peer', <ChannelPeerAlias id={out_channel} />)}
{renderLine('In Channel', in_channel)}
{renderLine('Out Channel', out_channel)}
{renderLine('Fee', fee ? formatSats(fee) : null)}
</>
),
options
);
addEvent({
title: 'Successful Forward',
summary: fee ? [{ label: 'Fee', value: formatSats(fee) }] : [],
details: [
{ label: 'In Peer', value: <ChannelPeerAlias id={in_channel} /> },
{ label: 'Out Peer', value: <ChannelPeerAlias id={out_channel} /> },
{ label: 'In Channel', value: in_channel },
{ label: 'Out Channel', value: out_channel },
],
});
handleRefetchQueries(['GetForwards']);
}
},
[handleRefetchQueries, forwardAttempts, forwards, options]
[handleRefetchQueries, forwardAttempts, forwards, addEvent]
);
const handleClosed = useCallback(
@ -233,28 +203,31 @@ export const useListener = (disabled?: boolean) => {
return types.join(', ');
};
toast.success(
renderToast(
'Channel Closed',
<>
{renderLine('Reason', getCloseType())}
{renderLine('Capacity', formatSats(capacity))}
{renderLine('Id', id)}
{renderLine('Peer', <PeerAlias pubkey={partner_public_key} />)}
{renderLine(
'Tx',
transaction_id ? getTransactionLink(transaction_id) : null
)}
{renderLine(
'Closing Tx',
close_transaction_id
? getTransactionLink(close_transaction_id)
: null
)}
</>
),
options
);
addEvent({
title: 'Channel Closed',
summary: [
{ label: 'Reason', value: getCloseType() },
{ label: 'Capacity', value: formatSats(capacity) },
],
details: [
{ label: 'Id', value: id },
{
label: 'Peer',
value: <PeerAlias pubkey={partner_public_key} />,
},
...(transaction_id
? [{ label: 'Tx', value: getTransactionLink(transaction_id) }]
: []),
...(close_transaction_id
? [
{
label: 'Closing Tx',
value: getTransactionLink(close_transaction_id),
},
]
: []),
],
});
handleRefetchQueries([
'GetChannels',
@ -262,7 +235,7 @@ export const useListener = (disabled?: boolean) => {
'GetClosedChannels',
]);
},
[handleRefetchQueries, channels, options]
[handleRefetchQueries, channels, addEvent]
);
const handleOpen = useCallback(
@ -279,43 +252,47 @@ export const useListener = (disabled?: boolean) => {
is_private,
} = message;
toast.success(
renderToast(
'Channel Opened',
<>
{renderLine(
'Initiated By',
is_partner_initiated ? 'Your Peer' : 'You'
)}
{renderLine('Id', id)}
{renderLine('Peer', <PeerAlias pubkey={partner_public_key} />)}
{renderLine('Private', is_private ? 'Yes' : 'No')}
{renderLine('Capacity', formatSats(capacity))}
{renderLine('Local', formatSats(local_balance))}
{renderLine('Remote', formatSats(remote_balance))}
</>
),
options
);
addEvent({
title: 'Channel Opened',
summary: [
{ label: 'Capacity', value: formatSats(capacity) },
{
label: 'Initiated By',
value: is_partner_initiated ? 'Your Peer' : 'You',
},
],
details: [
{ label: 'Id', value: id },
{
label: 'Peer',
value: <PeerAlias pubkey={partner_public_key} />,
},
{ label: 'Private', value: is_private ? 'Yes' : 'No' },
{ label: 'Local', value: formatSats(local_balance) },
{ label: 'Remote', value: formatSats(remote_balance) },
],
});
handleRefetchQueries(['GetChannels', 'GetPendingChannels']);
},
[handleRefetchQueries, channels, options]
[handleRefetchQueries, channels, addEvent]
);
const handleOpening = useCallback(
(message: any) => {
if (!channels) return;
toast.success(
renderToast(
'Channel Opening',
renderLine('Transaction', getTransactionLink(message.transaction_id))
),
options
);
addEvent({
title: 'Channel Opening',
summary: [
{
label: 'Transaction',
value: getTransactionLink(message.transaction_id),
},
],
});
handleRefetchQueries(['GetChannels', 'GetPendingChannels']);
},
[handleRefetchQueries, channels, options]
[handleRefetchQueries, channels, addEvent]
);
useSseEvent('invoice_updated', handleInvoice);

View file

@ -9,7 +9,7 @@ export const Footer = () => {
return (
<div className="absolute bottom-0 w-full h-[120px]">
<div className="dark w-full bg-background px-4">
<div className="dark w-full bg-background px-4 border-t border-border/60">
<div
className={
pathname === '/login'

View file

@ -121,20 +121,20 @@ export const BalancesContent = () => {
</div>
</div>
<div className="p-2">
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60 mb-2">
<div>
<div className="p-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60">
Liquidity
</div>
<div className="flex flex-col gap-1.5">
<div className="flex flex-col pb-2">
<button
className="flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/50"
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/50"
onClick={() => setOpenDialog('open')}
>
<Cable size={13} className="text-blue-500" />
Open Channel
</button>
<button
className="flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/50"
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/50"
onClick={() => setOpenDialog('buy')}
>
<Rocket size={13} className="text-orange-500" />

View file

@ -0,0 +1,271 @@
import { FC, useState, useEffect, useRef } from 'react';
import {
useEventLogState,
useEventLogDispatch,
useEventLog,
EventLogEntry,
EventField,
} from '../../context/EventLogContext';
import {
useNotificationState,
useNotificationDispatch,
} from '../../context/NotificationContext';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '../../components/ui/popover';
import { Switch } from '../../components/ui/switch';
import {
Trash2,
Zap,
ArrowRightLeft,
Cable,
FileText,
ChevronDown,
ChevronUp,
Plus,
Settings,
} from 'lucide-react';
import { fakeEvents } from './fakeEvents';
const formatRelativeTime = (timestamp: number): string => {
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 60) return 'now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
return `${Math.floor(hours / 24)}d`;
};
const getIcon = (title: string) => {
const t = title.toLowerCase();
if (t.includes('invoice'))
return <FileText size={12} className="text-purple-400" />;
if (t.includes('payment'))
return <Zap size={12} className="text-yellow-500" />;
if (t.includes('forward'))
return <ArrowRightLeft size={12} className="text-blue-400" />;
if (t.includes('channel'))
return <Cable size={12} className="text-green-400" />;
return <Zap size={12} className="text-yellow-500" />;
};
const FieldRow: FC<{ field: EventField }> = ({ field }) => {
if (!field.value) return null;
return (
<div className="flex justify-between gap-2 text-[11px] leading-snug">
<span className="text-muted-foreground/50 shrink-0">{field.label}</span>
<span className="text-right truncate">{field.value}</span>
</div>
);
};
const EventEntry: FC<{ entry: EventLogEntry; index: number }> = ({
entry,
index,
}) => {
const [expanded, setExpanded] = useState(false);
const hasDetails = entry.details.length > 0;
return (
<div className={`px-2 ${index % 2 === 1 ? 'bg-muted/30' : ''}`}>
<div className="flex items-start gap-2 py-1.5">
<span
className={`mt-0.5 shrink-0 ${
entry.status === 'error'
? 'text-red-400'
: 'text-muted-foreground/70'
}`}
>
{getIcon(entry.title)}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="text-[11px] font-medium truncate text-foreground">
{entry.title}
</span>
<span className="text-[10px] tabular-nums text-muted-foreground/40 shrink-0">
{formatRelativeTime(entry.timestamp)}
</span>
</div>
<div className="mt-0.5 text-muted-foreground">
{entry.summary.map((f, i) => (
<FieldRow key={i} field={f} />
))}
</div>
{hasDetails && (
<>
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-0.5 mt-0.5 text-[10px] text-muted-foreground/40 hover:text-muted-foreground transition-colors"
>
<ChevronDown
size={10}
className={`transition-transform ${expanded ? 'rotate-180' : ''}`}
/>
{expanded ? 'Less' : 'More'}
</button>
{expanded && (
<div className="mt-0.5 text-muted-foreground [&_a]:text-blue-400 [&_a]:hover:underline">
{entry.details.map((f, i) => (
<FieldRow key={i} field={f} />
))}
</div>
)}
</>
)}
</div>
</div>
</div>
);
};
const NotificationToggles: FC = () => {
const { invoices, payments, channels, forwards, forwardAttempts } =
useNotificationState();
const dispatch = useNotificationDispatch();
const items = [
{ label: 'Invoices', property: 'invoices', value: invoices },
{ label: 'Payments', property: 'payments', value: payments },
{ label: 'Channels', property: 'channels', value: channels },
{ label: 'Forwards', property: 'forwards', value: forwards },
{
label: 'Forward Attempts',
property: 'forwardAttempts',
value: forwardAttempts,
},
];
return (
<div className="space-y-2">
<div className="text-[11px] font-medium text-muted-foreground">
Enable event types
</div>
{items.map(item => (
<div key={item.property} className="flex items-center justify-between">
<span className="text-[11px]">{item.label}</span>
<Switch
checked={item.value}
onCheckedChange={checked =>
dispatch({ type: 'change', [item.property]: checked })
}
/>
</div>
))}
</div>
);
};
export const EventLog: FC = () => {
const { entries } = useEventLogState();
const dispatch = useEventLogDispatch();
const { addEvent } = useEventLog();
const [collapsed, setCollapsed] = useState(false);
const [ping, setPing] = useState(false);
const prevCount = useRef(entries.length);
useEffect(() => {
if (entries.length > prevCount.current) {
setPing(true);
const t = setTimeout(() => setPing(false), 2000);
return () => clearTimeout(t);
}
prevCount.current = entries.length;
}, [entries.length]);
useEffect(() => {
prevCount.current = entries.length;
}, [entries.length]);
const { invoices, payments, channels, forwards, forwardAttempts } =
useNotificationState();
const anyEnabled =
invoices || payments || channels || forwards || forwardAttempts;
const pushFakeEvent = () => {
const event = fakeEvents[Math.floor(Math.random() * fakeEvents.length)];
addEvent(event);
};
return (
<div className="flex flex-col flex-1 min-h-0">
<div className="flex items-center justify-between p-2 border-y border-border/60">
<button
onClick={() => setCollapsed(!collapsed)}
className="flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60 hover:text-muted-foreground transition-colors"
>
{collapsed ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
Events
{entries.length > 0 && (
<span className="text-muted-foreground/40 flex items-center gap-2">
{entries.length}
{ping ? (
<span className="relative flex size-2">
<span className="absolute inline-flex size-2 animate-ping rounded-full bg-purple-300 opacity-75" />
<span className="absolute inline-flex size-2 rounded-full bg-green-500" />
</span>
) : (
<span className="size-2 rounded-full bg-accent" />
)}
</span>
)}
</button>
<div className="flex items-center gap-1.5">
<Popover>
<PopoverTrigger asChild>
<button
className="text-muted-foreground/40 hover:text-muted-foreground transition-colors"
title="Notification settings"
>
<Settings size={11} />
</button>
</PopoverTrigger>
<PopoverContent align="end" className="w-52">
<NotificationToggles />
</PopoverContent>
</Popover>
{entries.length > 0 && (
<button
onClick={() => dispatch({ type: 'clear' })}
className="text-muted-foreground/40 hover:text-muted-foreground transition-colors"
title="Clear all"
>
<Trash2 size={11} />
</button>
)}
{import.meta.env.DEV && (
<button
onClick={pushFakeEvent}
className="text-muted-foreground/40 hover:text-muted-foreground transition-colors"
title="Add fake event"
>
<Plus size={11} />
</button>
)}
</div>
</div>
{!collapsed && (
<div className="flex-1 overflow-y-auto overflow-x-hidden">
{!anyEnabled ? (
<div className="flex flex-col items-center justify-center py-8 gap-1.5 text-muted-foreground/40">
<Settings size={16} />
<span className="text-[11px]">No events enabled</span>
</div>
) : entries.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 gap-1.5 text-muted-foreground/40">
<Zap size={16} />
<span className="text-[11px]">No events yet</span>
</div>
) : (
entries.map((entry, i) => (
<EventEntry key={entry.id} entry={entry} index={i} />
))
)}
</div>
)}
</div>
);
};

View file

@ -1,5 +1,6 @@
import { useConfigState } from '../../context/ConfigContext';
import { BalancesContent } from './BalancesContent';
import { EventLog } from './EventLog';
export const RightSidebar = () => {
const { rightSidebar } = useConfigState();
@ -8,10 +9,13 @@ export const RightSidebar = () => {
return (
<div className="hidden lg:flex flex-col shrink-0 w-[320px] border-l border-border/60">
<div className="sticky top-[77px]">
<div className="w-[320px]">
<div className="sticky top-[77px] flex flex-col max-h-[calc(100vh-77px)]">
<div className="w-[320px] shrink-0">
<BalancesContent />
</div>
<div className="flex-1 min-h-0 w-[320px] flex flex-col">
<EventLog />
</div>
</div>
</div>
);

View file

@ -0,0 +1,76 @@
import { EventField } from '../../context/EventLogContext';
export const fakeEvents: {
title: string;
summary: EventField[];
details?: EventField[];
status?: 'success' | 'error';
}[] = [
{
title: 'Invoice Paid',
summary: [{ label: 'Amount', value: '50,000 sats' }],
details: [
{ label: 'Description', value: 'Monthly VPN subscription' },
{ label: 'Description Hash', value: 'a1b2c3...d4e5f6' },
],
},
{
title: 'New Payment',
summary: [
{ label: 'Amount', value: '125,000 sats' },
{ label: 'Destination', value: 'ACINQ' },
],
details: [
{ label: 'Fee', value: '12 sats' },
{ label: 'Hop 1', value: '824333x2100x1' },
{ label: 'Hop 2', value: '710421x1500x0' },
],
},
{
title: 'Successful Forward',
summary: [{ label: 'Fee', value: '3 sats' }],
details: [
{ label: 'In Peer', value: 'WalletOfSatoshi' },
{ label: 'Out Peer', value: 'River Financial' },
{ label: 'In Channel', value: '810111x2300x1' },
{ label: 'Out Channel', value: '799000x1800x0' },
],
},
{
title: 'Forward Attempt',
summary: [{ label: 'Tokens', value: '2,000,000 sats' }],
details: [
{ label: 'In Peer', value: 'Kraken' },
{ label: 'Out Peer', value: 'Bitfinex' },
{ label: 'In Channel', value: '800100x500x2' },
{ label: 'Out Channel', value: '800200x600x1' },
{ label: 'Fee', value: '150 sats' },
],
status: 'error',
},
{
title: 'Channel Closed',
summary: [
{ label: 'Reason', value: 'Cooperative' },
{ label: 'Capacity', value: '5,000,000 sats' },
],
details: [
{ label: 'Id', value: '750000x1200x0' },
{ label: 'Peer', value: 'LNBig' },
],
},
{
title: 'Channel Opened',
summary: [
{ label: 'Capacity', value: '10,000,000 sats' },
{ label: 'Initiated By', value: 'Your Peer' },
],
details: [
{ label: 'Id', value: '812000x900x1' },
{ label: 'Peer', value: 'Voltage' },
{ label: 'Private', value: 'No' },
{ label: 'Local', value: '0 sats' },
{ label: 'Remote', value: '10,000,000 sats' },
],
},
];