import React from "react"; import { ConnectPeerRequest, MempoolUtxo, NewChannelOrder, OpenChannelRequest, OpenChannelResponse, PayInvoiceResponse, } from "src/types"; import { CopyIcon, QrCodeIcon, RefreshCwIcon } from "lucide-react"; import { Link } from "react-router"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; import ExternalLink from "src/components/ExternalLink"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; import { Button } from "src/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "src/components/ui/card"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "src/components/ui/dialog"; import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { Separator } from "src/components/ui/separator"; import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "src/components/ui/tooltip"; import { useBalances } from "src/hooks/useBalances"; import { ChannelWaitingForConfirmations } from "src/components/channels/ChannelWaitingForConfirmations"; import { PayLightningInvoice } from "src/components/PayLightningInvoice"; import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; import { LinkButton } from "src/components/ui/custom/link-button"; import { useChannels } from "src/hooks/useChannels"; import { useMempoolApi } from "src/hooks/useMempoolApi"; import { useNodeDetails } from "src/hooks/useNodeDetails"; import { useOnchainAddress } from "src/hooks/useOnchainAddress"; import { usePeers } from "src/hooks/usePeers"; import { useSyncWallet } from "src/hooks/useSyncWallet"; import { copyToClipboard } from "src/lib/clipboard"; import { splitSocketAddress } from "src/lib/utils"; import useChannelOrderStore from "src/state/ChannelOrderStore"; import { LSPOrderRequest, LSPOrderResponse } from "src/types"; import { request } from "src/utils/request"; // ensures React does not open a duplicate channel // this is a hack and will break if the user tries to open // 2 outbound channels without refreshing the page (I think an edge case) let hasStartedOpenedChannel = false; export function CurrentChannelOrder() { const order = useChannelOrderStore((store) => store.order); if (!order) { return (

No pending channel order.{" "} Return to channels page

); } return ; } function ChannelOrderInternal({ order }: { order: NewChannelOrder }) { useSyncWallet(); switch (order.status) { case "pay": switch (order.paymentMethod) { case "onchain": return ; case "lightning": return ; default: break; } break; case "paid": // LSPS1 only return ; case "opening": return ; case "success": return ; default: break; } return (

TODO: {order.status} {order.paymentMethod}

); } function Success() { return (

Congratulations! Your channel is active and can be used to send and receive payments.

To ensure you can both send and receive, make sure to balance your{" "} channel's liquidity .

Go to your dashboard
); } function ChannelOpening({ fundingTxId }: { fundingTxId: string | undefined }) { const { data: channels } = useChannels(true); const channel = fundingTxId ? channels?.find((channel) => channel.fundingTxId === fundingTxId) : undefined; React.useEffect(() => { if (channel?.active) { useChannelOrderStore.getState().updateOrder({ status: "success", }); } }, [channel]); if (!channel) { return ; } return ; } function useEstimatedTransactionFeeSat() { const { data: recommendedFees } = useMempoolApi<{ fastestFee: number }>( "/v1/fees/recommended", true ); if (recommendedFees?.fastestFee) { // estimated transaction size: 200 vbytes return 200 * recommendedFees.fastestFee; } } // TODO: move these to new files function PayBitcoinChannelOrder({ order }: { order: NewChannelOrder }) { if (order.paymentMethod !== "onchain") { throw new Error("incorrect payment method"); } const { data: balances } = useBalances(true); if (!balances) { return ; } // expect at least the user to have more funds than the channel size, hopefully enough to cover mempool fees. if (balances.onchain.spendableSat > +order.amountSat) { return ; } if (balances.onchain.totalSat > +order.amountSat) { return ; } return ; } function PayBitcoinChannelOrderWaitingDepositConfirmation() { return ( <> Bitcoin deposited Waiting for one block confirmation estimated time: 10 minutes ); } function PayBitcoinChannelOrderTopup({ order }: { order: NewChannelOrder }) { if (order.paymentMethod !== "onchain") { throw new Error("incorrect payment method"); } const { data: channels } = useChannels(); const { data: balances } = useBalances(); const { data: onchainAddress, getNewAddress, loadingAddress, } = useOnchainAddress(); const { data: mempoolAddressUtxos } = useMempoolApi( onchainAddress ? `/address/${onchainAddress}/utxo` : undefined, 3000 ); const estimatedTransactionFeeSat = useEstimatedTransactionFeeSat(); if (!onchainAddress || !balances || !estimatedTransactionFeeSat) { return (
); } // expect at least the user to have more funds than the channel size, hopefully enough to cover mempool fees. // This only considers one UTXO and will not work well if the user generates a new address. // However, this is just a fallback because LDK only updates onchain balances ~ once per minute. const unspentAmountSat = mempoolAddressUtxos?.map((utxo) => utxo.value).reduce((a, b) => a + b, 0) || 0; if (unspentAmountSat > +order.amountSat) { return ; } const num0ConfChannels = channels?.filter((c) => c.confirmationsRequired === 0).length || 0; const estimatedAnchorReserveSat = Math.max( num0ConfChannels * 25000 - balances.onchain.reservedSat, 0 ); const missingAmountSat = +order.amountSat + estimatedTransactionFeeSat + estimatedAnchorReserveSat - balances.onchain.totalSat; const recommendedAmountSat = Math.ceil(missingAmountSat / 10000) * 10000; const topupLink = `https://getalby.com/topup?address=${onchainAddress}&receive_amount=${recommendedAmountSat}`; return (

You currently have{" "} . We recommend depositing an additional amount of{" "} {" "} to open this channel.

This amount includes cost for the channel opening and potential channel onchain reserves.

Deposit bitcoin Scan this QR code with your wallet to send funds. {!loadingAddress && } Generate a new address
Waiting for your transaction Send a bitcoin transaction to the address provided above. You'll be redirected as soon as the transaction is seen in the mempool. {unspentAmountSat > 0 && ( {" "} deposited )} Top up with your credit card or bank account Need receiving capacity?
); } function PayBitcoinChannelOrderWithSpendableFunds({ order, }: { order: NewChannelOrder; }) { if (order.paymentMethod !== "onchain") { throw new Error("incorrect payment method"); } const { data: peers } = usePeers(); const { pubkey, host } = order; const { data: nodeDetails } = useNodeDetails(pubkey); const connectPeer = React.useCallback(async () => { if (!nodeDetails && !host) { throw new Error("node details not found"); } const socketAddress = nodeDetails?.sockets ? nodeDetails.sockets.split(",")[0] : host; const { address, port } = splitSocketAddress(socketAddress); if (!address || !port) { throw new Error("host not found"); } console.info(`🔌 Peering with ${pubkey}`); const connectPeerRequest: ConnectPeerRequest = { pubkey, address, port: +port, }; await request("/api/peers", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(connectPeerRequest), }); }, [nodeDetails, pubkey, host]); const openChannel = React.useCallback(async () => { try { if (order.paymentMethod !== "onchain") { throw new Error("incorrect payment method"); } if (!peers) { throw new Error("peers not loaded"); } // only pair if necessary // also allows to open channel to existing peer without providing a socket address. if (!peers.some((peer) => peer.nodeId === pubkey)) { await connectPeer(); } console.info(`🎬 Opening channel with ${pubkey}`); const openChannelRequest: OpenChannelRequest = { pubkey, amountSats: +order.amountSat, public: order.isPublic, }; const openChannelResponse = await request( "/api/channels", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(openChannelRequest), } ); if (!openChannelResponse?.fundingTxId) { throw new Error("No funding txid in response"); } console.info( "Channel opening transaction published", openChannelResponse.fundingTxId ); toast("Successfully published channel opening transaction"); useChannelOrderStore.getState().updateOrder({ fundingTxId: openChannelResponse.fundingTxId, status: "opening", }); } catch (error) { console.error(error); toast.error("Something went wrong", { description: "" + error, }); } }, [ connectPeer, order.amountSat, order.isPublic, order.paymentMethod, peers, pubkey, ]); React.useEffect(() => { if (!peers || hasStartedOpenedChannel) { return; } hasStartedOpenedChannel = true; openChannel(); }, [openChannel, order.amountSat, peers, pubkey]); return (

Please wait...

); } function useWaitForNewChannel() { const order = useChannelOrderStore((store) => store.order); const { data: channels } = useChannels(true); const newChannel = channels && order?.prevChannelIds ? channels.find( (newChannel) => !order.prevChannelIds.some( (current) => newChannel.id === current ) && newChannel.fundingTxId ) : undefined; React.useEffect(() => { if (newChannel) { useChannelOrderStore.getState().updateOrder({ status: "opening", fundingTxId: newChannel.fundingTxId, }); } }, [newChannel]); } function PaidLightningChannelOrder() { useWaitForNewChannel(); return (

Waiting for channel to be opened...

); } function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) { if (order.paymentMethod !== "lightning") { throw new Error("incorrect payment method"); } const { data: balances } = useBalances(); const { data: channels } = useChannels(true); const [, setRequestedInvoice] = React.useState(false); const [lspOrderResponse, setLspOrderResponse] = React.useState< LSPOrderResponse | undefined >(); useWaitForNewChannel(); React.useEffect(() => { if (!channels) { return; } setRequestedInvoice((current) => { if (!current) { (async () => { try { if (!order.lspType || !order.lspIdentifier) { throw new Error("missing lsp info in order"); } const newLSPOrderRequest: LSPOrderRequest = { lspType: order.lspType, lspIdentifier: order.lspIdentifier, amountSat: parseInt(order.amountSat), public: order.isPublic, }; const response = await request( "/api/lsp-orders", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(newLSPOrderRequest), } ); if (!response) { throw new Error("no LSP order response"); } if (!response.invoice) { // assume payment is handled by Alby Account // we will wait for a channel to be opened to us useChannelOrderStore.getState().updateOrder({ status: "paid", }); } setLspOrderResponse(response); } catch (error) { toast.error("Something went wrong", { description: "" + error, }); } })(); } return true; }); }, [ channels, order.amountSat, order.isPublic, order.lspType, order.lspIdentifier, ]); const canPayInternally = balances && lspOrderResponse && balances.lightning.nextMaxSpendableMPPMsat / 1000 > lspOrderResponse.invoiceAmountSat; const [isPaying, setPaying] = React.useState(false); const [payExternally, setPayExternally] = React.useState(false); return (
{!lspOrderResponse?.invoice && } {lspOrderResponse?.invoice && ( <>
{lspOrderResponse.outgoingLiquiditySat > 0 && ( Lightning Balance )} {lspOrderResponse.incomingLiquiditySat > 0 && ( Incoming Liquidity
)} Amount to pay

By proceeding, you consent the channel opens immediately and that you lose the right to revoke once it is open.

<> {canPayInternally && (
{ try { setPaying(true); // NOTE: for amboss this will not return until the HOLD invoice is settled // which is after the channel has N block confirmations await request( `/api/payments/${lspOrderResponse.invoice}`, { method: "POST", headers: { "Content-Type": "application/json", }, } ); useChannelOrderStore.getState().updateOrder({ status: "paid", }); toast("Channel successfully requested"); } catch (e) { toast.error("Failed to send: ", { description: "" + e, }); console.error(e); } setPaying(false); }} > Pay and open channel {!payExternally && ( )}
)} {(payExternally || !canPayInternally) && (
)}

Other options

Increase Lightning Balance Buy Bitcoin
)}
); }