CloseChannelDialogContent.tsx raw

   1  import {
   2    AlertCircleIcon,
   3    AlertTriangleIcon,
   4    CopyIcon,
   5    ExternalLinkIcon,
   6  } from "lucide-react";
   7  import React from "react";
   8  import { toast } from "sonner";
   9  import { SwapAlert } from "src/components/channels/SwapAlert";
  10  import ExternalLink from "src/components/ExternalLink";
  11  import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
  12  import { MempoolAlert } from "src/components/MempoolAlert";
  13  import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
  14  import { Button } from "src/components/ui/button";
  15  import { Label } from "src/components/ui/label";
  16  import { RadioGroup, RadioGroupItem } from "src/components/ui/radio-group";
  17  import { useBalances } from "src/hooks/useBalances";
  18  import { useChannels } from "src/hooks/useChannels";
  19  import { useInfo } from "src/hooks/useInfo";
  20  import { copyToClipboard } from "src/lib/clipboard";
  21  import { Channel, CloseChannelResponse } from "src/types";
  22  import { request } from "src/utils/request";
  23  import {
  24    AlertDialogCancel,
  25    AlertDialogContent,
  26    AlertDialogDescription,
  27    AlertDialogFooter,
  28    AlertDialogHeader,
  29    AlertDialogTitle,
  30  } from "./ui/alert-dialog";
  31  
  32  type Props = {
  33    alias: string;
  34    channel: Channel;
  35  };
  36  
  37  export function CloseChannelDialogContent({ alias, channel }: Props) {
  38    const [closeType, setCloseType] = React.useState("normal");
  39    const [step, setStep] = React.useState(channel.active ? 2 : 1);
  40    const [fundingTxId, setFundingTxId] = React.useState("");
  41    const { data: info } = useInfo();
  42    const { mutate: reloadBalances } = useBalances();
  43    const { data: channels, mutate: reloadChannels } = useChannels();
  44  
  45    const onContinue = () => {
  46      setStep(step + 1);
  47    };
  48  
  49    const copy = (text: string) => {
  50      copyToClipboard(text);
  51    };
  52  
  53    async function closeChannel() {
  54      try {
  55        console.info(`🎬 Closing channel with ${channel.remotePubkey}`);
  56  
  57        const closeChannelResponse = await request<CloseChannelResponse>(
  58          `/api/peers/${channel.remotePubkey}/channels/${channel.id}?force=${
  59            closeType === "force"
  60          }`,
  61          {
  62            method: "DELETE",
  63            headers: {
  64              "Content-Type": "application/json",
  65            },
  66          }
  67        );
  68  
  69        if (!closeChannelResponse) {
  70          throw new Error("Error closing channel");
  71        }
  72  
  73        const closedChannel = channels?.find(
  74          (c) => c.id === channel.id && c.remotePubkey === channel.remotePubkey
  75        );
  76        console.info("Closed channel", closedChannel);
  77        if (closedChannel) {
  78          setFundingTxId(closedChannel.fundingTxId);
  79          setStep(step + 1);
  80        }
  81        toast("Successfully closed channel");
  82      } catch (error) {
  83        console.error(error);
  84        toast.error("Something went wrong: " + error);
  85      }
  86    }
  87  
  88    return (
  89      <AlertDialogContent>
  90        {step === 1 && (
  91          <>
  92            <AlertDialogHeader>
  93              <AlertDialogTitle>
  94                Are you sure you want to close the channel with {alias}?
  95              </AlertDialogTitle>
  96              <AlertDialogDescription>
  97                This channel is inactive. Some channels require up to 6 onchain
  98                confirmations before they are usable.
  99              </AlertDialogDescription>
 100            </AlertDialogHeader>
 101            <AlertDialogFooter>
 102              <AlertDialogCancel>Cancel</AlertDialogCancel>
 103              <Button onClick={onContinue}>Confirm</Button>
 104            </AlertDialogFooter>
 105          </>
 106        )}
 107  
 108        {step === 2 && (
 109          <>
 110            <AlertDialogHeader>
 111              <AlertDialogTitle>
 112                Are you sure you want to close the channel with {alias}?
 113              </AlertDialogTitle>
 114              <AlertDialogDescription className="text-left">
 115                <SwapAlert minChannels={0} className="mb-4" />
 116                <Alert className="mb-4">
 117                  <AlertCircleIcon className="h-4 w-4" />
 118                  <AlertDescription>
 119                    <div>
 120                      Closing this channel will move{" "}
 121                      <FormattedBitcoinAmount
 122                        amountMsat={channel.localBalanceMsat}
 123                      />{" "}
 124                      in this channel to your on-chain balance and reduce your
 125                      receive limit by{" "}
 126                      <FormattedBitcoinAmount
 127                        amountMsat={channel.remoteBalanceMsat}
 128                      />
 129                      .
 130                    </div>
 131                  </AlertDescription>
 132                </Alert>
 133                <div>
 134                  <p className="font-medium text-foreground">Node ID</p>
 135                  <p className="break-all">{channel.remotePubkey}</p>
 136                </div>
 137                <div className="mt-4">
 138                  <p className="font-medium text-foreground">Channel ID</p>
 139                  <p className="break-all">{channel.id}</p>
 140                </div>
 141              </AlertDialogDescription>
 142            </AlertDialogHeader>
 143            <AlertDialogFooter>
 144              <AlertDialogCancel>Cancel</AlertDialogCancel>
 145              <Button onClick={onContinue}>Continue</Button>
 146            </AlertDialogFooter>
 147          </>
 148        )}
 149  
 150        {step === 3 && (
 151          <>
 152            <AlertDialogHeader>
 153              <AlertDialogTitle>Select mode of channel closure</AlertDialogTitle>
 154              <AlertDialogDescription className="text-left">
 155                <div className="mb-4">
 156                  <MempoolAlert />
 157                </div>
 158                {closeType === "force" && (
 159                  <Alert className="mb-4">
 160                    <AlertTriangleIcon className="h-4 w-4" />
 161                    <AlertTitle>Heads up!</AlertTitle>
 162                    <AlertDescription>
 163                      Your channel balance will be locked for up to two weeks if
 164                      you force close
 165                    </AlertDescription>
 166                  </Alert>
 167                )}
 168                <RadioGroup
 169                  defaultValue="normal"
 170                  value={closeType}
 171                  onValueChange={() =>
 172                    setCloseType(closeType === "normal" ? "force" : "normal")
 173                  }
 174                  className="mt-2"
 175                >
 176                  <div className="flex items-start space-x-2 mb-2">
 177                    <RadioGroupItem
 178                      value="normal"
 179                      id="normal"
 180                      className="shrink-0"
 181                    />
 182                    <div className="grid gap-1.5">
 183                      <Label
 184                        htmlFor="normal"
 185                        className="text-foreground cursor-pointer"
 186                      >
 187                        Normal Close (Recommended)
 188                      </Label>
 189                      <p className="text-sm text-muted-foreground">
 190                        Attempt to settle with your channel partner for a quick,
 191                        low-cost closure. Your funds should be available on-chain
 192                        within an hour. If your partner is offline or agreement
 193                        cannot be met, a force closure will be initiated.
 194                      </p>
 195                    </div>
 196                  </div>
 197                  <div className="flex items-start space-x-2">
 198                    <RadioGroupItem
 199                      value="force"
 200                      id="force"
 201                      className="shrink-0"
 202                    />
 203                    <div className="grid gap-1.5">
 204                      <Label
 205                        htmlFor="force"
 206                        className="text-foreground cursor-pointer"
 207                      >
 208                        Force Close
 209                      </Label>
 210                      <p className="text-sm text-muted-foreground">
 211                        You close the channel alone. Your funds may be locked for
 212                        up to two weeks and may incur higher fees. Only try this
 213                        if a normal closure does not work.
 214                      </p>
 215                    </div>
 216                  </div>
 217                </RadioGroup>
 218                <ExternalLink
 219                  to="https://guides.getalby.com/user-guide/alby-hub/faq/how-can-i-close-a-channel-what-happens-to-the-sats-in-this-channel"
 220                  className="underline flex items-center mt-4"
 221                >
 222                  Learn more about closing channels
 223                  <ExternalLinkIcon className="size-4 ml-2" />
 224                </ExternalLink>
 225              </AlertDialogDescription>
 226            </AlertDialogHeader>
 227            <AlertDialogFooter>
 228              <AlertDialogCancel>Cancel</AlertDialogCancel>
 229              <Button onClick={closeChannel}>Close Channel</Button>
 230            </AlertDialogFooter>
 231          </>
 232        )}
 233  
 234        {step === 4 && (
 235          <>
 236            <AlertDialogHeader>
 237              <AlertDialogTitle>Channel closed successfully</AlertDialogTitle>
 238              <AlertDialogDescription className="text-left">
 239                <p className="font-medium text-foreground">
 240                  Funding Transaction Id
 241                </p>
 242                <div className="flex items-center justify-between gap-4">
 243                  <p className="break-all">{fundingTxId}</p>
 244                  <CopyIcon
 245                    className="cursor-pointer text-muted-foreground size-4"
 246                    onClick={() => {
 247                      copy(fundingTxId);
 248                    }}
 249                  />
 250                </div>
 251                <ExternalLink
 252                  to={`${info?.mempoolUrl}/tx/${fundingTxId}`}
 253                  className="underline flex items-center mt-2"
 254                >
 255                  View on Mempool
 256                  <ExternalLinkIcon className="size-4 ml-2" />
 257                </ExternalLink>
 258              </AlertDialogDescription>
 259            </AlertDialogHeader>
 260            <AlertDialogFooter>
 261              <AlertDialogCancel
 262                onClick={async () => {
 263                  await reloadChannels();
 264                  await reloadBalances();
 265                }}
 266              >
 267                Done
 268              </AlertDialogCancel>
 269            </AlertDialogFooter>
 270          </>
 271        )}
 272      </AlertDialogContent>
 273    );
 274  }
 275