ChannelsTable.tsx raw

   1  import { InfoIcon } from "lucide-react";
   2  import { ChannelWarning } from "src/components/channels/ChannelWarning";
   3  import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
   4  import Loading from "src/components/Loading.tsx";
   5  import { Badge } from "src/components/ui/badge.tsx";
   6  import {
   7    Card,
   8    CardContent,
   9    CardHeader,
  10    CardTitle,
  11  } from "src/components/ui/card";
  12  import { Progress } from "src/components/ui/progress.tsx";
  13  import {
  14    Table,
  15    TableBody,
  16    TableCell,
  17    TableHead,
  18    TableHeader,
  19    TableRow,
  20  } from "src/components/ui/table.tsx";
  21  import {
  22    Tooltip,
  23    TooltipContent,
  24    TooltipProvider,
  25    TooltipTrigger,
  26  } from "src/components/ui/tooltip.tsx";
  27  import { useNodeDetails } from "src/hooks/useNodeDetails";
  28  import { Channel, LongUnconfirmedZeroConfChannel } from "src/types";
  29  import { ChannelDropdownMenu } from "./ChannelDropdownMenu";
  30  
  31  type ChannelsTableProps = {
  32    channels?: Channel[];
  33    longUnconfirmedZeroConfChannels: LongUnconfirmedZeroConfChannel[];
  34  };
  35  
  36  export function ChannelsTable({
  37    channels,
  38    longUnconfirmedZeroConfChannels,
  39  }: ChannelsTableProps) {
  40    if (channels && !channels.length) {
  41      return null;
  42    }
  43  
  44    return (
  45      <Card className="hidden lg:block">
  46        <CardHeader>
  47          <CardTitle className="text-2xl">Channels</CardTitle>
  48        </CardHeader>
  49        <CardContent>
  50          <Table>
  51            <TableHeader>
  52              <TableRow>
  53                <TableHead className="w-[160px] text-muted-foreground">
  54                  Peer
  55                </TableHead>
  56                <TableHead className="w-[80px] text-muted-foreground">
  57                  <TooltipProvider>
  58                    <Tooltip>
  59                      <TooltipTrigger>
  60                        <div className="flex flex-row gap-1 items-center text-muted-foreground">
  61                          Type
  62                          <InfoIcon className="h-3 w-3 shrink-0" />
  63                        </div>
  64                      </TooltipTrigger>
  65                      <TooltipContent>
  66                        The type of lightning channel, By default private channel
  67                        is recommended. If you a podcaster or musician and expect
  68                        to receive keysend or Value4Value payments you will need a
  69                        public channel.
  70                      </TooltipContent>
  71                    </Tooltip>
  72                  </TooltipProvider>
  73                </TableHead>
  74                <TableHead className="w-[80px] text-muted-foreground">
  75                  Status
  76                </TableHead>
  77                <TableHead className="w-[128px] text-muted-foreground">
  78                  <TooltipProvider>
  79                    <Tooltip>
  80                      <TooltipTrigger>
  81                        <div className="flex flex-row gap-1 items-center text-muted-foreground">
  82                          Capacity
  83                          <InfoIcon className="h-3 w-3 shrink-0" />
  84                        </div>
  85                      </TooltipTrigger>
  86                      <TooltipContent>
  87                        Total Spending and Receiving capacity of your lightning
  88                        channel.
  89                      </TooltipContent>
  90                    </Tooltip>
  91                  </TooltipProvider>
  92                </TableHead>
  93                <TableHead className="w-[128px]">
  94                  <TooltipProvider>
  95                    <Tooltip>
  96                      <TooltipTrigger>
  97                        <div className="flex flex-row gap-1 items-center text-muted-foreground">
  98                          Reserve
  99                          <InfoIcon className="h-3 w-3 shrink-0" />
 100                        </div>
 101                      </TooltipTrigger>
 102                      <TooltipContent>
 103                        Funds each participant sets aside to discourage cheating
 104                        by ensuring each party has something at stake. This
 105                        reserve cannot be spent during the channel's lifetime and
 106                        typically amounts to 1% of the channel capacity. The
 107                        reserve will automatically be filled when payments are
 108                        received on the channel.
 109                      </TooltipContent>
 110                    </Tooltip>
 111                  </TooltipProvider>
 112                </TableHead>
 113                <TableHead className="w-[350px]">
 114                  <div className="flex flex-row justify-between items-center gap-2 text-muted-foreground">
 115                    <div>Spending</div>
 116                    <div>Receiving</div>
 117                  </div>
 118                </TableHead>
 119                <TableHead className="w-px"></TableHead>
 120                <TableHead className="w-[50px]"></TableHead>
 121              </TableRow>
 122            </TableHeader>
 123            <TableBody>
 124              {channels && channels.length > 0 && (
 125                <>
 126                  {channels
 127                    .sort((a, b) =>
 128                      a.localBalanceMsat + a.remoteBalanceMsat >
 129                      b.localBalanceMsat + b.remoteBalanceMsat
 130                        ? -1
 131                        : 1
 132                    )
 133                    .map((channel) => {
 134                      const unconfirmedChannel =
 135                        longUnconfirmedZeroConfChannels.find(
 136                          (uc) => uc.id === channel.id
 137                        );
 138                      return (
 139                        <ChannelTableRow
 140                          key={channel.id}
 141                          channel={channel}
 142                          unconfirmedChannel={unconfirmedChannel}
 143                          hasMultipleChannels={channels.length > 1}
 144                        />
 145                      );
 146                    })}
 147                </>
 148              )}
 149              {!channels && (
 150                <TableRow>
 151                  <TableCell colSpan={6}>
 152                    <Loading className="m-2" />
 153                  </TableCell>
 154                </TableRow>
 155              )}
 156            </TableBody>
 157          </Table>
 158        </CardContent>
 159      </Card>
 160    );
 161  }
 162  
 163  type ChannelTableRowProps = {
 164    channel: Channel;
 165    unconfirmedChannel: LongUnconfirmedZeroConfChannel | undefined;
 166    hasMultipleChannels: boolean;
 167  };
 168  
 169  function ChannelTableRow({
 170    channel,
 171    unconfirmedChannel,
 172    hasMultipleChannels,
 173  }: ChannelTableRowProps) {
 174    const { data: peerDetails } = useNodeDetails(channel.remotePubkey);
 175    const capacityMsat = channel.localBalanceMsat + channel.remoteBalanceMsat;
 176    const alias = peerDetails?.alias || "Unknown";
 177  
 178    return (
 179      <TableRow key={channel.id} className="channel">
 180        <TableCell>
 181          <span className="font-semibold text-sm mr-2">{alias}</span>
 182        </TableCell>
 183        <TableCell>{channel.public ? "Public" : "Private"}</TableCell>
 184        <TableCell>
 185          {channel.status == "online" ? (
 186            unconfirmedChannel ? (
 187              <Badge variant="outline" title={unconfirmedChannel.message}>
 188                Unconfirmed
 189              </Badge>
 190            ) : (
 191              <Badge variant="positive">Online</Badge>
 192            )
 193          ) : channel.status == "opening" ? (
 194            <Badge variant="outline">Opening</Badge>
 195          ) : (
 196            <Badge variant="warning">Offline</Badge>
 197          )}
 198        </TableCell>
 199        <TableCell>
 200          <FormattedBitcoinAmount amountMsat={capacityMsat} />
 201        </TableCell>
 202        <TableCell title={channel.unspendablePunishmentReserveSat + " sats"}>
 203          {channel.localBalanceMsat <
 204            channel.unspendablePunishmentReserveSat * 1000 && (
 205            <>
 206              <FormattedBitcoinAmount
 207                amountMsat={Math.min(
 208                  channel.localBalanceMsat,
 209                  channel.unspendablePunishmentReserveSat * 1000
 210                )}
 211                showSymbol={false}
 212              />{" "}
 213              /{" "}
 214            </>
 215          )}
 216          <FormattedBitcoinAmount
 217            amountMsat={channel.unspendablePunishmentReserveSat * 1000}
 218          />
 219        </TableCell>
 220        <TableCell>
 221          <div className="relative">
 222            <Progress
 223              value={(channel.localSpendableBalanceMsat / capacityMsat) * 100}
 224              className="h-6 absolute"
 225            />
 226            <div className="flex flex-row w-full justify-between px-2 text-xs items-center h-6 mix-blend-exclusion text-white">
 227              <span>
 228                <FormattedBitcoinAmount
 229                  amountMsat={channel.localSpendableBalanceMsat}
 230                />
 231              </span>
 232              <span>
 233                <FormattedBitcoinAmount amountMsat={channel.remoteBalanceMsat} />
 234              </span>
 235            </div>
 236          </div>
 237        </TableCell>
 238        <TableCell>
 239          <ChannelWarning channel={channel} />
 240        </TableCell>
 241        <TableCell>
 242          <ChannelDropdownMenu
 243            alias={alias}
 244            channel={channel}
 245            hasMultipleChannels={hasMultipleChannels}
 246          />
 247        </TableCell>
 248      </TableRow>
 249    );
 250  }
 251