CurrentChannelOrder.tsx raw

   1  import React from "react";
   2  import {
   3    ConnectPeerRequest,
   4    MempoolUtxo,
   5    NewChannelOrder,
   6    OpenChannelRequest,
   7    OpenChannelResponse,
   8    PayInvoiceResponse,
   9  } from "src/types";
  10  
  11  import { CopyIcon, QrCodeIcon, RefreshCwIcon } from "lucide-react";
  12  import { Link } from "react-router";
  13  import { toast } from "sonner";
  14  import AppHeader from "src/components/AppHeader";
  15  import ExternalLink from "src/components/ExternalLink";
  16  import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
  17  import FormattedFiatAmount from "src/components/FormattedFiatAmount";
  18  import Loading from "src/components/Loading";
  19  import QRCode from "src/components/QRCode";
  20  import { Button } from "src/components/ui/button";
  21  import {
  22    Card,
  23    CardContent,
  24    CardDescription,
  25    CardFooter,
  26    CardHeader,
  27    CardTitle,
  28  } from "src/components/ui/card";
  29  import { LoadingButton } from "src/components/ui/custom/loading-button";
  30  import {
  31    Dialog,
  32    DialogContent,
  33    DialogDescription,
  34    DialogHeader,
  35    DialogTitle,
  36    DialogTrigger,
  37  } from "src/components/ui/dialog";
  38  import { Input } from "src/components/ui/input";
  39  import { Label } from "src/components/ui/label";
  40  import { Separator } from "src/components/ui/separator";
  41  import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table";
  42  import {
  43    Tooltip,
  44    TooltipContent,
  45    TooltipProvider,
  46    TooltipTrigger,
  47  } from "src/components/ui/tooltip";
  48  import { useBalances } from "src/hooks/useBalances";
  49  
  50  import { ChannelWaitingForConfirmations } from "src/components/channels/ChannelWaitingForConfirmations";
  51  import { PayLightningInvoice } from "src/components/PayLightningInvoice";
  52  import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader";
  53  import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
  54  import { LinkButton } from "src/components/ui/custom/link-button";
  55  import { useChannels } from "src/hooks/useChannels";
  56  import { useMempoolApi } from "src/hooks/useMempoolApi";
  57  import { useNodeDetails } from "src/hooks/useNodeDetails";
  58  import { useOnchainAddress } from "src/hooks/useOnchainAddress";
  59  import { usePeers } from "src/hooks/usePeers";
  60  import { useSyncWallet } from "src/hooks/useSyncWallet";
  61  import { copyToClipboard } from "src/lib/clipboard";
  62  import { splitSocketAddress } from "src/lib/utils";
  63  import useChannelOrderStore from "src/state/ChannelOrderStore";
  64  import { LSPOrderRequest, LSPOrderResponse } from "src/types";
  65  import { request } from "src/utils/request";
  66  
  67  // ensures React does not open a duplicate channel
  68  // this is a hack and will break if the user tries to open
  69  // 2 outbound channels without refreshing the page (I think an edge case)
  70  let hasStartedOpenedChannel = false;
  71  
  72  export function CurrentChannelOrder() {
  73    const order = useChannelOrderStore((store) => store.order);
  74    if (!order) {
  75      return (
  76        <p>
  77          No pending channel order.{" "}
  78          <Link to="/channels" className="underline">
  79            Return to channels page
  80          </Link>
  81        </p>
  82      );
  83    }
  84    return <ChannelOrderInternal order={order} />;
  85  }
  86  
  87  function ChannelOrderInternal({ order }: { order: NewChannelOrder }) {
  88    useSyncWallet();
  89    switch (order.status) {
  90      case "pay":
  91        switch (order.paymentMethod) {
  92          case "onchain":
  93            return <PayBitcoinChannelOrder order={order} />;
  94          case "lightning":
  95            return <PayLightningChannelOrder order={order} />;
  96          default:
  97            break;
  98        }
  99        break;
 100      case "paid":
 101        // LSPS1 only
 102        return <PaidLightningChannelOrder />;
 103      case "opening":
 104        return <ChannelOpening fundingTxId={order.fundingTxId} />;
 105      case "success":
 106        return <Success />;
 107      default:
 108        break;
 109    }
 110  
 111    return (
 112      <p>
 113        TODO: {order.status} {order.paymentMethod}
 114      </p>
 115    );
 116  }
 117  
 118  function Success() {
 119    return (
 120      <div className="flex flex-col justify-center gap-5 p-5 max-w-md items-stretch">
 121        <TwoColumnLayoutHeader
 122          title="Channel Opened"
 123          pageTitle="Channel Opened"
 124          description="Your new lightning channel is ready to use"
 125        />
 126  
 127        <p>
 128          Congratulations! Your channel is active and can be used to send and
 129          receive payments.
 130        </p>
 131        <p>
 132          To ensure you can both send and receive, make sure to balance your{" "}
 133          <ExternalLink
 134            to="https://guides.getalby.com/user-guide/alby-hub/node"
 135            className="underline"
 136          >
 137            channel's liquidity
 138          </ExternalLink>
 139          .
 140        </p>
 141  
 142        <LinkButton to="/home" className="flex justify-center mt-8">
 143          Go to your dashboard
 144        </LinkButton>
 145      </div>
 146    );
 147  }
 148  
 149  function ChannelOpening({ fundingTxId }: { fundingTxId: string | undefined }) {
 150    const { data: channels } = useChannels(true);
 151    const channel = fundingTxId
 152      ? channels?.find((channel) => channel.fundingTxId === fundingTxId)
 153      : undefined;
 154  
 155    React.useEffect(() => {
 156      if (channel?.active) {
 157        useChannelOrderStore.getState().updateOrder({
 158          status: "success",
 159        });
 160      }
 161    }, [channel]);
 162  
 163    if (!channel) {
 164      return <Loading />;
 165    }
 166  
 167    return <ChannelWaitingForConfirmations channel={channel} />;
 168  }
 169  
 170  function useEstimatedTransactionFeeSat() {
 171    const { data: recommendedFees } = useMempoolApi<{ fastestFee: number }>(
 172      "/v1/fees/recommended",
 173      true
 174    );
 175    if (recommendedFees?.fastestFee) {
 176      // estimated transaction size: 200 vbytes
 177      return 200 * recommendedFees.fastestFee;
 178    }
 179  }
 180  
 181  // TODO: move these to new files
 182  function PayBitcoinChannelOrder({ order }: { order: NewChannelOrder }) {
 183    if (order.paymentMethod !== "onchain") {
 184      throw new Error("incorrect payment method");
 185    }
 186    const { data: balances } = useBalances(true);
 187  
 188    if (!balances) {
 189      return <Loading />;
 190    }
 191  
 192    // expect at least the user to have more funds than the channel size, hopefully enough to cover mempool fees.
 193    if (balances.onchain.spendableSat > +order.amountSat) {
 194      return <PayBitcoinChannelOrderWithSpendableFunds order={order} />;
 195    }
 196    if (balances.onchain.totalSat > +order.amountSat) {
 197      return <PayBitcoinChannelOrderWaitingDepositConfirmation />;
 198    }
 199    return <PayBitcoinChannelOrderTopup order={order} />;
 200  }
 201  
 202  function PayBitcoinChannelOrderWaitingDepositConfirmation() {
 203    return (
 204      <>
 205        <Card>
 206          <CardHeader>
 207            <CardTitle className="flex flex-row items-center gap-2">
 208              Bitcoin deposited
 209            </CardTitle>
 210          </CardHeader>
 211          <CardContent className="flex items-center gap-2">
 212            <Loading /> Waiting for one block confirmation
 213          </CardContent>
 214          <CardFooter className="text-muted-foreground">
 215            estimated time: 10 minutes
 216          </CardFooter>
 217        </Card>
 218      </>
 219    );
 220  }
 221  
 222  function PayBitcoinChannelOrderTopup({ order }: { order: NewChannelOrder }) {
 223    if (order.paymentMethod !== "onchain") {
 224      throw new Error("incorrect payment method");
 225    }
 226  
 227    const { data: channels } = useChannels();
 228  
 229    const { data: balances } = useBalances();
 230    const {
 231      data: onchainAddress,
 232      getNewAddress,
 233      loadingAddress,
 234    } = useOnchainAddress();
 235  
 236    const { data: mempoolAddressUtxos } = useMempoolApi<MempoolUtxo[]>(
 237      onchainAddress ? `/address/${onchainAddress}/utxo` : undefined,
 238      3000
 239    );
 240    const estimatedTransactionFeeSat = useEstimatedTransactionFeeSat();
 241  
 242    if (!onchainAddress || !balances || !estimatedTransactionFeeSat) {
 243      return (
 244        <div className="flex justify-center">
 245          <Loading />
 246        </div>
 247      );
 248    }
 249  
 250    // expect at least the user to have more funds than the channel size, hopefully enough to cover mempool fees.
 251    // This only considers one UTXO and will not work well if the user generates a new address.
 252    // However, this is just a fallback because LDK only updates onchain balances ~ once per minute.
 253    const unspentAmountSat =
 254      mempoolAddressUtxos?.map((utxo) => utxo.value).reduce((a, b) => a + b, 0) ||
 255      0;
 256  
 257    if (unspentAmountSat > +order.amountSat) {
 258      return <PayBitcoinChannelOrderWaitingDepositConfirmation />;
 259    }
 260  
 261    const num0ConfChannels =
 262      channels?.filter((c) => c.confirmationsRequired === 0).length || 0;
 263  
 264    const estimatedAnchorReserveSat = Math.max(
 265      num0ConfChannels * 25000 - balances.onchain.reservedSat,
 266      0
 267    );
 268  
 269    const missingAmountSat =
 270      +order.amountSat +
 271      estimatedTransactionFeeSat +
 272      estimatedAnchorReserveSat -
 273      balances.onchain.totalSat;
 274  
 275    const recommendedAmountSat = Math.ceil(missingAmountSat / 10000) * 10000;
 276    const topupLink = `https://getalby.com/topup?address=${onchainAddress}&receive_amount=${recommendedAmountSat}`;
 277  
 278    return (
 279      <div className="grid gap-5">
 280        <AppHeader
 281          pageTitle="Deposit bitcoin"
 282          title="Deposit bitcoin"
 283          description="You don't have enough Bitcoin to open your intended channel"
 284        />
 285        <div className="grid gap-5 max-w-lg">
 286          <div className="grid gap-1.5">
 287            <Label htmlFor="text">On-Chain Address</Label>
 288            <p className="text-xs slashed-zero">
 289              You currently have{" "}
 290              <span className="font-semibold sensitive">
 291                <FormattedBitcoinAmount
 292                  amountMsat={balances.onchain.totalSat * 1000}
 293                />
 294              </span>
 295              . We recommend depositing an additional amount of{" "}
 296              <span className="font-semibold">
 297                <FormattedBitcoinAmount
 298                  amountMsat={recommendedAmountSat * 1000}
 299                />
 300              </span>{" "}
 301              to open this channel.
 302            </p>
 303            <p className="text-xs text-muted-foreground">
 304              This amount includes cost for the channel opening and potential
 305              channel onchain reserves.
 306            </p>
 307            <div className="flex flex-row gap-2 items-center">
 308              <Input
 309                type="text"
 310                value={onchainAddress}
 311                readOnly
 312                className="flex-1"
 313              />
 314              <Button
 315                variant="secondary"
 316                size="icon"
 317                onClick={() => {
 318                  copyToClipboard(onchainAddress);
 319                }}
 320              >
 321                <CopyIcon className="size-4" />
 322              </Button>
 323              <Dialog>
 324                <DialogTrigger asChild>
 325                  <Button variant="secondary" size="icon">
 326                    <QrCodeIcon className="size-4" />
 327                  </Button>
 328                </DialogTrigger>
 329                <DialogContent>
 330                  <DialogHeader>
 331                    <DialogTitle>Deposit bitcoin</DialogTitle>
 332                    <DialogDescription>
 333                      Scan this QR code with your wallet to send funds.
 334                    </DialogDescription>
 335                  </DialogHeader>
 336                  <div className="flex flex-row justify-center p-3">
 337                    <a href={`bitcoin:${onchainAddress}`} target="_blank">
 338                      <QRCode value={onchainAddress} paymentType="onchain" />
 339                    </a>
 340                  </div>
 341                </DialogContent>
 342              </Dialog>
 343              <TooltipProvider>
 344                <Tooltip>
 345                  <TooltipTrigger asChild>
 346                    <LoadingButton
 347                      variant="secondary"
 348                      size="icon"
 349                      onClick={getNewAddress}
 350                      loading={loadingAddress}
 351                      className="w-9 h-9"
 352                    >
 353                      {!loadingAddress && <RefreshCwIcon className="size-4" />}
 354                    </LoadingButton>
 355                  </TooltipTrigger>
 356                  <TooltipContent>Generate a new address</TooltipContent>
 357                </Tooltip>
 358              </TooltipProvider>
 359            </div>
 360          </div>
 361  
 362          <Card>
 363            <CardHeader>
 364              <CardTitle className="flex flex-row items-center gap-2">
 365                <Loading /> Waiting for your transaction
 366              </CardTitle>
 367              <CardDescription>
 368                Send a bitcoin transaction to the address provided above. You'll
 369                be redirected as soon as the transaction is seen in the mempool.
 370              </CardDescription>
 371            </CardHeader>
 372            {unspentAmountSat > 0 && (
 373              <CardContent className="slashed-zero">
 374                <FormattedBitcoinAmount amountMsat={unspentAmountSat * 1000} />{" "}
 375                deposited
 376              </CardContent>
 377            )}
 378          </Card>
 379  
 380          <ExternalLinkButton to={topupLink} className="w-full">
 381            Top up with your credit card or bank account
 382          </ExternalLinkButton>
 383          <LinkButton
 384            to="/channels/incoming"
 385            variant="secondary"
 386            className="w-full"
 387          >
 388            Need receiving capacity?
 389          </LinkButton>
 390        </div>
 391      </div>
 392    );
 393  }
 394  
 395  function PayBitcoinChannelOrderWithSpendableFunds({
 396    order,
 397  }: {
 398    order: NewChannelOrder;
 399  }) {
 400    if (order.paymentMethod !== "onchain") {
 401      throw new Error("incorrect payment method");
 402    }
 403    const { data: peers } = usePeers();
 404  
 405    const { pubkey, host } = order;
 406  
 407    const { data: nodeDetails } = useNodeDetails(pubkey);
 408  
 409    const connectPeer = React.useCallback(async () => {
 410      if (!nodeDetails && !host) {
 411        throw new Error("node details not found");
 412      }
 413      const socketAddress = nodeDetails?.sockets
 414        ? nodeDetails.sockets.split(",")[0]
 415        : host;
 416  
 417      const { address, port } = splitSocketAddress(socketAddress);
 418  
 419      if (!address || !port) {
 420        throw new Error("host not found");
 421      }
 422      console.info(`🔌 Peering with ${pubkey}`);
 423      const connectPeerRequest: ConnectPeerRequest = {
 424        pubkey,
 425        address,
 426        port: +port,
 427      };
 428      await request("/api/peers", {
 429        method: "POST",
 430        headers: {
 431          "Content-Type": "application/json",
 432        },
 433        body: JSON.stringify(connectPeerRequest),
 434      });
 435    }, [nodeDetails, pubkey, host]);
 436  
 437    const openChannel = React.useCallback(async () => {
 438      try {
 439        if (order.paymentMethod !== "onchain") {
 440          throw new Error("incorrect payment method");
 441        }
 442  
 443        if (!peers) {
 444          throw new Error("peers not loaded");
 445        }
 446  
 447        // only pair if necessary
 448        // also allows to open channel to existing peer without providing a socket address.
 449        if (!peers.some((peer) => peer.nodeId === pubkey)) {
 450          await connectPeer();
 451        }
 452  
 453        console.info(`🎬 Opening channel with ${pubkey}`);
 454  
 455        const openChannelRequest: OpenChannelRequest = {
 456          pubkey,
 457          amountSats: +order.amountSat,
 458          public: order.isPublic,
 459        };
 460        const openChannelResponse = await request<OpenChannelResponse>(
 461          "/api/channels",
 462          {
 463            method: "POST",
 464            headers: {
 465              "Content-Type": "application/json",
 466            },
 467            body: JSON.stringify(openChannelRequest),
 468          }
 469        );
 470  
 471        if (!openChannelResponse?.fundingTxId) {
 472          throw new Error("No funding txid in response");
 473        }
 474        console.info(
 475          "Channel opening transaction published",
 476          openChannelResponse.fundingTxId
 477        );
 478        toast("Successfully published channel opening transaction");
 479        useChannelOrderStore.getState().updateOrder({
 480          fundingTxId: openChannelResponse.fundingTxId,
 481          status: "opening",
 482        });
 483      } catch (error) {
 484        console.error(error);
 485        toast.error("Something went wrong", {
 486          description: "" + error,
 487        });
 488      }
 489    }, [
 490      connectPeer,
 491      order.amountSat,
 492      order.isPublic,
 493      order.paymentMethod,
 494      peers,
 495      pubkey,
 496    ]);
 497  
 498    React.useEffect(() => {
 499      if (!peers || hasStartedOpenedChannel) {
 500        return;
 501      }
 502  
 503      hasStartedOpenedChannel = true;
 504      openChannel();
 505    }, [openChannel, order.amountSat, peers, pubkey]);
 506  
 507    return (
 508      <div className="flex flex-col gap-5">
 509        <AppHeader
 510          pageTitle="Opening channel"
 511          title="Opening channel"
 512          description="Your funds have been successfully deposited"
 513        />
 514  
 515        <div className="flex flex-col gap-5">
 516          <Loading />
 517          <p>Please wait...</p>
 518        </div>
 519      </div>
 520    );
 521  }
 522  
 523  function useWaitForNewChannel() {
 524    const order = useChannelOrderStore((store) => store.order);
 525    const { data: channels } = useChannels(true);
 526  
 527    const newChannel =
 528      channels && order?.prevChannelIds
 529        ? channels.find(
 530            (newChannel) =>
 531              !order.prevChannelIds.some(
 532                (current) => newChannel.id === current
 533              ) && newChannel.fundingTxId
 534          )
 535        : undefined;
 536  
 537    React.useEffect(() => {
 538      if (newChannel) {
 539        useChannelOrderStore.getState().updateOrder({
 540          status: "opening",
 541          fundingTxId: newChannel.fundingTxId,
 542        });
 543      }
 544    }, [newChannel]);
 545  }
 546  
 547  function PaidLightningChannelOrder() {
 548    useWaitForNewChannel();
 549  
 550    return (
 551      <div className="flex w-full h-full gap-2 items-center justify-center">
 552        <Loading /> <p>Waiting for channel to be opened...</p>
 553      </div>
 554    );
 555  }
 556  
 557  function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
 558    if (order.paymentMethod !== "lightning") {
 559      throw new Error("incorrect payment method");
 560    }
 561    const { data: balances } = useBalances();
 562    const { data: channels } = useChannels(true);
 563    const [, setRequestedInvoice] = React.useState(false);
 564  
 565    const [lspOrderResponse, setLspOrderResponse] = React.useState<
 566      LSPOrderResponse | undefined
 567    >();
 568  
 569    useWaitForNewChannel();
 570  
 571    React.useEffect(() => {
 572      if (!channels) {
 573        return;
 574      }
 575      setRequestedInvoice((current) => {
 576        if (!current) {
 577          (async () => {
 578            try {
 579              if (!order.lspType || !order.lspIdentifier) {
 580                throw new Error("missing lsp info in order");
 581              }
 582              const newLSPOrderRequest: LSPOrderRequest = {
 583                lspType: order.lspType,
 584                lspIdentifier: order.lspIdentifier,
 585                amountSat: parseInt(order.amountSat),
 586                public: order.isPublic,
 587              };
 588              const response = await request<LSPOrderResponse>(
 589                "/api/lsp-orders",
 590                {
 591                  method: "POST",
 592                  headers: {
 593                    "Content-Type": "application/json",
 594                  },
 595                  body: JSON.stringify(newLSPOrderRequest),
 596                }
 597              );
 598              if (!response) {
 599                throw new Error("no LSP order response");
 600              }
 601  
 602              if (!response.invoice) {
 603                // assume payment is handled by Alby Account
 604                // we will wait for a channel to be opened to us
 605                useChannelOrderStore.getState().updateOrder({
 606                  status: "paid",
 607                });
 608              }
 609              setLspOrderResponse(response);
 610            } catch (error) {
 611              toast.error("Something went wrong", {
 612                description: "" + error,
 613              });
 614            }
 615          })();
 616        }
 617        return true;
 618      });
 619    }, [
 620      channels,
 621      order.amountSat,
 622      order.isPublic,
 623      order.lspType,
 624      order.lspIdentifier,
 625    ]);
 626  
 627    const canPayInternally =
 628      balances &&
 629      lspOrderResponse &&
 630      balances.lightning.nextMaxSpendableMPPMsat / 1000 >
 631        lspOrderResponse.invoiceAmountSat;
 632    const [isPaying, setPaying] = React.useState(false);
 633    const [payExternally, setPayExternally] = React.useState(false);
 634  
 635    return (
 636      <div className="flex flex-col gap-5">
 637        <AppHeader
 638          pageTitle="Review Channel Purchase"
 639          title="Review Channel Purchase"
 640          description={
 641            lspOrderResponse
 642              ? "Complete Payment to open a channel to your node"
 643              : "Please wait, loading..."
 644          }
 645        />
 646        {!lspOrderResponse?.invoice && <Loading />}
 647  
 648        {lspOrderResponse?.invoice && (
 649          <>
 650            <div className="max-w-md flex flex-col gap-5">
 651              <div className="border rounded-lg slashed-zero">
 652                <Table>
 653                  <TableBody>
 654                    {lspOrderResponse.outgoingLiquiditySat > 0 && (
 655                      <TableRow>
 656                        <TableCell className="font-medium p-3">
 657                          Lightning Balance
 658                        </TableCell>
 659                        <TableCell className="text-right p-3">
 660                          <FormattedBitcoinAmount
 661                            amountMsat={
 662                              lspOrderResponse.outgoingLiquiditySat * 1000
 663                            }
 664                          />
 665                        </TableCell>
 666                      </TableRow>
 667                    )}
 668                    {lspOrderResponse.incomingLiquiditySat > 0 && (
 669                      <TableRow>
 670                        <TableCell className="font-medium p-3">
 671                          Incoming Liquidity
 672                        </TableCell>
 673                        <TableCell className="text-right p-3">
 674                          <div className="flex flex-col items-end">
 675                            <FormattedBitcoinAmount
 676                              amountMsat={
 677                                lspOrderResponse.incomingLiquiditySat * 1000
 678                              }
 679                            />
 680                            <FormattedFiatAmount
 681                              amountSat={lspOrderResponse.incomingLiquiditySat}
 682                              showApprox
 683                            />
 684                          </div>
 685                        </TableCell>
 686                      </TableRow>
 687                    )}
 688                    <TableRow>
 689                      <TableCell className="font-medium p-3">
 690                        Amount to pay
 691                      </TableCell>
 692                      <TableCell className="font-semibold text-right p-3">
 693                        <div className="flex flex-col items-end">
 694                          <FormattedBitcoinAmount
 695                            amountMsat={lspOrderResponse.invoiceAmountSat * 1000}
 696                          />
 697                          <FormattedFiatAmount
 698                            amountSat={lspOrderResponse.invoiceAmountSat}
 699                            showApprox
 700                          />
 701                        </div>
 702                      </TableCell>
 703                    </TableRow>
 704                  </TableBody>
 705                </Table>
 706              </div>
 707              <div className="flex justify-center w-full -mb-5">
 708                <p className="text-center text-xs text-muted-foreground max-w-sm">
 709                  By proceeding, you consent the channel opens immediately and
 710                  that you lose the right to revoke once it is open.
 711                </p>
 712              </div>
 713              <>
 714                {canPayInternally && (
 715                  <div className="flex flex-col gap-2">
 716                    <LoadingButton
 717                      loading={isPaying}
 718                      className="mt-4"
 719                      onClick={async () => {
 720                        try {
 721                          setPaying(true);
 722  
 723                          // NOTE: for amboss this will not return until the HOLD invoice is settled
 724                          // which is after the channel has N block confirmations
 725                          await request<PayInvoiceResponse>(
 726                            `/api/payments/${lspOrderResponse.invoice}`,
 727                            {
 728                              method: "POST",
 729                              headers: {
 730                                "Content-Type": "application/json",
 731                              },
 732                            }
 733                          );
 734  
 735                          useChannelOrderStore.getState().updateOrder({
 736                            status: "paid",
 737                          });
 738                          toast("Channel successfully requested");
 739                        } catch (e) {
 740                          toast.error("Failed to send: ", {
 741                            description: "" + e,
 742                          });
 743                          console.error(e);
 744                        }
 745                        setPaying(false);
 746                      }}
 747                    >
 748                      Pay and open channel
 749                    </LoadingButton>
 750                    {!payExternally && (
 751                      <Button
 752                        type="button"
 753                        variant="link"
 754                        className="text-muted-foreground"
 755                        onClick={() => setPayExternally(true)}
 756                      >
 757                        Pay with another wallet
 758                      </Button>
 759                    )}
 760                  </div>
 761                )}
 762  
 763                {(payExternally || !canPayInternally) && (
 764                  <div className="flex flex-row justify-center">
 765                    <PayLightningInvoice invoice={lspOrderResponse.invoice} />
 766                  </div>
 767                )}
 768  
 769                <div className="flex-1 flex flex-col justify-end items-center gap-4">
 770                  <Separator className="mt-4 mb-6" />
 771                  <p className="text-sm text-muted-foreground text-center">
 772                    Other options
 773                  </p>
 774                  <LinkButton
 775                    to="/channels/outgoing"
 776                    variant="secondary"
 777                    className="w-full"
 778                  >
 779                    Increase Lightning Balance
 780                  </LinkButton>
 781                  <ExternalLinkButton
 782                    to="https://www.getalby.com/topup"
 783                    variant="secondary"
 784                    className="w-full"
 785                  >
 786                    Buy Bitcoin
 787                  </ExternalLinkButton>
 788                </div>
 789              </>
 790            </div>
 791          </>
 792        )}
 793      </div>
 794    );
 795  }
 796