diff --git a/frontend/src/components/CloseChannelDialog.tsx b/frontend/src/components/CloseChannelDialog.tsx new file mode 100644 index 00000000..123dfe0a --- /dev/null +++ b/frontend/src/components/CloseChannelDialog.tsx @@ -0,0 +1,243 @@ +import { AlertTriangleIcon, CopyIcon, ExternalLinkIcon } from "lucide-react"; +import React from "react"; +import ExternalLink from "src/components/ExternalLink"; +import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; +import { Button } from "src/components/ui/button"; +import { Checkbox } from "src/components/ui/checkbox"; +import { toast } from "src/components/ui/use-toast"; +import { useChannels } from "src/hooks/useChannels"; +import { useCSRF } from "src/hooks/useCSRF"; +import { copyToClipboard } from "src/lib/clipboard"; +import { Channel, CloseChannelResponse } from "src/types"; +import { request } from "src/utils/request"; +import { + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "./ui/alert-dialog"; + +type Props = { + alias: string; + channel: Channel; +}; + +export function CloseChannelDialog({ alias, channel }: Props) { + const [closeType, setCloseType] = React.useState("normal"); + const [step, setStep] = React.useState(channel.active ? 2 : 1); + const [fundingTxId, setFundingTxId] = React.useState(""); + const { data: channels, mutate: reloadChannels } = useChannels(); + const { data: csrf } = useCSRF(); + + const onContinue = () => { + setStep(step + 1); + }; + + const copy = (text: string) => { + copyToClipboard(text); + toast({ title: "Copied to clipboard." }); + }; + + async function closeChannel() { + try { + if (!csrf) { + throw new Error("csrf not loaded"); + } + + console.info(`🎬 Closing channel with ${channel.remotePubkey}`); + + const closeChannelResponse = await request( + `/api/peers/${channel.remotePubkey}/channels/${channel.id}?force=${ + closeType === "force" + }`, + { + method: "DELETE", + headers: { + "X-CSRF-Token": csrf, + "Content-Type": "application/json", + }, + } + ); + + if (!closeChannelResponse) { + throw new Error("Error closing channel"); + } + + const closedChannel = channels?.find( + (c) => c.id === channel.id && c.remotePubkey === channel.remotePubkey + ); + console.info("Closed channel", closedChannel); + if (closedChannel) { + setFundingTxId(closedChannel.fundingTxId); + setStep(step + 1); + } + toast({ title: "Sucessfully closed channel" }); + } catch (error) { + console.error(error); + toast({ + variant: "destructive", + description: "Something went wrong: " + error, + }); + } + } + + return ( + + {step === 1 && ( + <> + + + Are you sure you want to close the channel with {alias}? + + + This channel is inactive. Some channels require up to 6 onchain + confirmations before they are usable. Proceed only if you still + want to continue + + + + Cancel + + + + )} + + {step === 2 && ( + <> + + + Are you sure you want to close the channel with {alias}? + + +
+

Node ID

+

{channel.remotePubkey}

+
+
+

Channel ID

+

{channel.id}

+
+
+
+ + Cancel + + + + )} + + {step === 3 && ( + <> + + Select mode of channel closure + + {closeType === "force" && ( + + + Heads up! + + Your channel balance will be locked for up to two weeks if + you force close + + + )} +
+
+ + setCloseType(closeType === "normal" ? "force" : "normal") + } + checked={closeType === "normal"} + /> +
+ +

+ Closes the channel cooperatively, usually faster and with + lower fees +

+
+
+
+ + setCloseType(closeType === "force" ? "normal" : "force") + } + checked={closeType === "force"} + /> +
+ +

+ Closes the channel unilaterally, can take longer and might + incur higher fees +

+
+
+
+ + Learn more about closing channels + + +
+
+ + Cancel + + + + )} + + {step === 4 && ( + <> + + Channel closed successfully + +

Funding Transaction Id

+
+

{fundingTxId}

+ { + copy(fundingTxId); + }} + /> +
+ + View on Mempool + + +
+
+ + { + await reloadChannels(); + }} + > + Done + + + + )} +
+ ); +} diff --git a/frontend/src/components/channels/ChannelsCards.tsx b/frontend/src/components/channels/ChannelsCards.tsx index 164e13d0..39ac1fad 100644 --- a/frontend/src/components/channels/ChannelsCards.tsx +++ b/frontend/src/components/channels/ChannelsCards.tsx @@ -6,7 +6,12 @@ import { Trash2, } from "lucide-react"; import { ChannelWarning } from "src/components/channels/ChannelWarning"; +import { CloseChannelDialog } from "src/components/CloseChannelDialog"; import ExternalLink from "src/components/ExternalLink"; +import { + AlertDialog, + AlertDialogTrigger, +} from "src/components/ui/alert-dialog"; import { Badge } from "src/components/ui/badge.tsx"; import { Button } from "src/components/ui/button.tsx"; import { @@ -36,18 +41,12 @@ import { Channel, Node } from "src/types"; type ChannelsCardsProps = { channels?: Channel[]; nodes?: Node[]; - closeChannel( - channelId: string, - counterpartyNodeId: string, - isActive: boolean - ): void; editChannel(channel: Channel): void; }; export function ChannelsCards({ channels, nodes, - closeChannel, editChannel, }: ChannelsCardsProps) { if (!channels?.length) { @@ -80,55 +79,51 @@ export function ChannelsCards({
{alias}
- - - - - - - - -

View Funding Transaction

-
-
- - - -

View Node on amboss.space

-
-
- {channel.public && ( - editChannel(channel)} - > - - Set Routing Fee + + + + + + + + + +

View Funding Transaction

+
- )} - - closeChannel( - channel.id, - channel.remotePubkey, - channel.active - ) - } - > - - Close Channel - -
-
+ + + +

View Node on amboss.space

+
+
+ {channel.public && ( + editChannel(channel)} + > + + Set Routing Fee + + )} + + + + Close Channel + + +
+
+ + diff --git a/frontend/src/components/channels/ChannelsTable.tsx b/frontend/src/components/channels/ChannelsTable.tsx index aaf9cec2..93b83ec8 100644 --- a/frontend/src/components/channels/ChannelsTable.tsx +++ b/frontend/src/components/channels/ChannelsTable.tsx @@ -6,8 +6,13 @@ import { Trash2, } from "lucide-react"; import { ChannelWarning } from "src/components/channels/ChannelWarning"; +import { CloseChannelDialog } from "src/components/CloseChannelDialog"; import ExternalLink from "src/components/ExternalLink"; import Loading from "src/components/Loading.tsx"; +import { + AlertDialog, + AlertDialogTrigger, +} from "src/components/ui/alert-dialog"; import { Badge } from "src/components/ui/badge.tsx"; import { Button } from "src/components/ui/button.tsx"; import { @@ -37,18 +42,12 @@ import { Channel, Node } from "src/types"; type ChannelsTableProps = { channels?: Channel[]; nodes?: Node[]; - closeChannel( - channelId: string, - counterpartyNodeId: string, - isActive: boolean - ): void; editChannel(channel: Channel): void; }; export function ChannelsTable({ channels, nodes, - closeChannel, editChannel, }: ChannelsTableProps) { if (channels && !channels.length) { @@ -176,55 +175,51 @@ export function ChannelsTable({ - - - - - - - - -

View Funding Transaction

-
-
- - - -

View Node on amboss.space

-
-
- {channel.public && ( - editChannel(channel)} - > - - Set Routing Fee + + + + + + + + + +

View Funding Transaction

+
- )} - - closeChannel( - channel.id, - channel.remotePubkey, - channel.active - ) - } - > - - Close Channel - -
-
+ + + +

View Node on amboss.space

+
+
+ {channel.public && ( + editChannel(channel)} + > + + Set Routing Fee + + )} + + + + Close Channel + + +
+
+ +
); diff --git a/frontend/src/screens/channels/Channels.tsx b/frontend/src/screens/channels/Channels.tsx index 4b180b19..fe77bb31 100644 --- a/frontend/src/screens/channels/Channels.tsx +++ b/frontend/src/screens/channels/Channels.tsx @@ -63,12 +63,7 @@ import { useRedeemOnchainFunds } from "src/hooks/useRedeemOnchainFunds.ts"; import { useSyncWallet } from "src/hooks/useSyncWallet.ts"; import { copyToClipboard } from "src/lib/clipboard.ts"; import { cn } from "src/lib/utils.ts"; -import { - Channel, - CloseChannelResponse, - Node, - UpdateChannelRequest, -} from "src/types"; +import { Channel, Node, UpdateChannelRequest } from "src/types"; import { request } from "src/utils/request"; import { useCSRF } from "../../hooks/useCSRF.ts"; @@ -114,85 +109,6 @@ export default function Channels() { loadNodeStats(); }, [loadNodeStats]); - async function closeChannel( - channelId: string, - nodeId: string, - isActive: boolean - ) { - try { - if (!csrf) { - throw new Error("csrf not loaded"); - } - if (!isActive) { - if ( - !confirm( - `This channel is inactive. Some channels require up to 6 onchain confirmations before they are usable. If you really want to continue, click OK.` - ) - ) { - return; - } - } - - if ( - !confirm( - `Are you sure you want to close the channel with ${ - nodes.find((node) => node.public_key === nodeId)?.alias || - "Unknown Node" - }?\n\nNode ID: ${nodeId}\n\nChannel ID: ${channelId}` - ) - ) { - return; - } - - const closeType = prompt( - "Select way to close the channel. Type 'force close' if you want to force close the channel. Note: your channel balance will be locked for up to two weeks if you force close.", - "normal close" - ); - if (!closeType) { - console.error("Cancelled close channel"); - return; - } - - console.info(`🎬 Closing channel with ${nodeId}`); - - const closeChannelResponse = await request( - `/api/peers/${nodeId}/channels/${channelId}?force=${ - closeType === "force close" - }`, - { - method: "DELETE", - headers: { - "X-CSRF-Token": csrf, - "Content-Type": "application/json", - }, - } - ); - - if (!closeChannelResponse) { - throw new Error("Error closing channel"); - } - - const closedChannel = channels?.find( - (c) => c.id === channelId && c.remotePubkey === nodeId - ); - console.info("Closed channel", closedChannel); - if (closedChannel) { - prompt( - "Closed channel. Copy channel funding TX to view on mempool", - closedChannel.fundingTxId - ); - } - await reloadChannels(); - toast({ title: "Sucessfully closed channel" }); - } catch (error) { - console.error(error); - toast({ - variant: "destructive", - description: "Something went wrong: " + error, - }); - } - } - async function editChannel(channel: Channel) { try { if (!csrf) { @@ -651,14 +567,12 @@ export default function Channels() { ) : ( )}