Skip to content

Commit

Permalink
feat: add close channel dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
im-adithya committed Jul 31, 2024
1 parent 406e9f3 commit 607258e
Show file tree
Hide file tree
Showing 4 changed files with 342 additions and 195 deletions.
243 changes: 243 additions & 0 deletions frontend/src/components/CloseChannelDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<CloseChannelResponse>(
`/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 (
<AlertDialogContent>
{step === 1 && (
<>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to close the channel with {alias}?
</AlertDialogTitle>
<AlertDialogDescription>
This channel is inactive. Some channels require up to 6 onchain
confirmations before they are usable. Proceed only if you still
want to continue
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button onClick={onContinue}>Continue</Button>
</AlertDialogFooter>
</>
)}

{step === 2 && (
<>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to close the channel with {alias}?
</AlertDialogTitle>
<AlertDialogDescription className="text-left">
<div>
<p className="text-primary font-medium">Node ID</p>
<p className="break-all">{channel.remotePubkey}</p>
</div>
<div className="mt-4">
<p className="text-primary font-medium">Channel ID</p>
<p className="break-all">{channel.id}</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button onClick={onContinue}>Continue</Button>
</AlertDialogFooter>
</>
)}

{step === 3 && (
<>
<AlertDialogHeader>
<AlertDialogTitle>Select mode of channel closure</AlertDialogTitle>
<AlertDialogDescription className="text-left">
{closeType === "force" && (
<Alert className="mb-4">
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
Your channel balance will be locked for up to two weeks if
you force close
</AlertDescription>
</Alert>
)}
<div className="flex flex-col gap-4 text-xs mt-2">
<div className="items-top flex space-x-2">
<Checkbox
id="normal"
onCheckedChange={() =>
setCloseType(closeType === "normal" ? "force" : "normal")
}
checked={closeType === "normal"}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="normal"
className="text-primary text-sm font-medium leading-none cursor-pointer"
>
Normal Close
</label>
<p className="text-sm text-muted-foreground">
Closes the channel cooperatively, usually faster and with
lower fees
</p>
</div>
</div>
<div className="items-top flex space-x-2">
<Checkbox
id="force"
onCheckedChange={() =>
setCloseType(closeType === "force" ? "normal" : "force")
}
checked={closeType === "force"}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="force"
className="text-primary text-sm font-medium leading-none cursor-pointer"
>
Force Close
</label>
<p className="text-sm text-muted-foreground">
Closes the channel unilaterally, can take longer and might
incur higher fees
</p>
</div>
</div>
</div>
<ExternalLink
to="https://guides.getalby.com/user-guide/v/alby-account-and-browser-extension/alby-hub/faq-alby-hub/how-can-i-close-this-channel-what-happens-to-the-sats-in-this-channel"
className="underline flex items-center mt-4"
>
Learn more about closing channels
<ExternalLinkIcon className="w-4 h-4 ml-2" />
</ExternalLink>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button onClick={closeChannel}>Close Channel</Button>
</AlertDialogFooter>
</>
)}

{step === 4 && (
<>
<AlertDialogHeader>
<AlertDialogTitle>Channel closed successfully</AlertDialogTitle>
<AlertDialogDescription className="text-left">
<p className="text-primary font-medium">Funding Transaction Id</p>
<div className="flex items-center justify-between gap-4">
<p className="break-all">{fundingTxId}</p>
<CopyIcon
className="cursor-pointer text-muted-foreground w-4 h-4"
onClick={() => {
copy(fundingTxId);
}}
/>
</div>
<ExternalLink
to={`https://mempool.space/tx/${fundingTxId}`}
className="underline flex items-center mt-2"
>
View on Mempool
<ExternalLinkIcon className="w-4 h-4 ml-2" />
</ExternalLink>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={async () => {
await reloadChannels();
}}
>
Done
</AlertDialogCancel>
</AlertDialogFooter>
</>
)}
</AlertDialogContent>
);
}
103 changes: 49 additions & 54 deletions frontend/src/components/channels/ChannelsCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -80,55 +79,51 @@ export function ChannelsCards({
<div className="flex-1 whitespace-nowrap text-ellipsis overflow-hidden">
{alias}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="flex flex-row items-center gap-2 cursor-pointer">
<ExternalLink
to={`https://mempool.space/tx/${channel.fundingTxId}`}
className="w-full flex flex-row items-center gap-2"
>
<ExternalLinkIcon className="w-4 h-4" />
<p>View Funding Transaction</p>
</ExternalLink>
</DropdownMenuItem>
<DropdownMenuItem className="flex flex-row items-center gap-2 cursor-pointer">
<ExternalLink
to={`https://amboss.space/node/${channel.remotePubkey}`}
className="w-full flex flex-row items-center gap-2"
>
<ExternalLinkIcon className="w-4 h-4" />
<p>View Node on amboss.space</p>
</ExternalLink>
</DropdownMenuItem>
{channel.public && (
<DropdownMenuItem
className="flex flex-row items-center gap-2 cursor-pointer"
onClick={() => editChannel(channel)}
>
<HandCoins className="h-4 w-4" />
Set Routing Fee
<AlertDialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="flex flex-row items-center gap-2 cursor-pointer">
<ExternalLink
to={`https://mempool.space/tx/${channel.fundingTxId}`}
className="w-full flex flex-row items-center gap-2"
>
<ExternalLinkIcon className="w-4 h-4" />
<p>View Funding Transaction</p>
</ExternalLink>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="flex flex-row items-center gap-2 cursor-pointer"
onClick={() =>
closeChannel(
channel.id,
channel.remotePubkey,
channel.active
)
}
>
<Trash2 className="h-4 w-4 text-destructive" />
Close Channel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuItem className="flex flex-row items-center gap-2 cursor-pointer">
<ExternalLink
to={`https://amboss.space/node/${channel.remotePubkey}`}
className="w-full flex flex-row items-center gap-2"
>
<ExternalLinkIcon className="w-4 h-4" />
<p>View Node on amboss.space</p>
</ExternalLink>
</DropdownMenuItem>
{channel.public && (
<DropdownMenuItem
className="flex flex-row items-center gap-2 cursor-pointer"
onClick={() => editChannel(channel)}
>
<HandCoins className="h-4 w-4" />
Set Routing Fee
</DropdownMenuItem>
)}
<AlertDialogTrigger asChild>
<DropdownMenuItem className="flex flex-row items-center gap-2 cursor-pointer">
<Trash2 className="h-4 w-4 text-destructive" />
Close Channel
</DropdownMenuItem>
</AlertDialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<CloseChannelDialog alias={alias} channel={channel} />
</AlertDialog>
</div>
</CardTitle>
<Separator className="mt-5" />
Expand Down
Loading

0 comments on commit 607258e

Please sign in to comment.