Skip to content

Commit

Permalink
feat: peers page and disconnect peer (#356)
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz authored May 30, 2024
1 parent 073d669 commit 16dc19e
Show file tree
Hide file tree
Showing 16 changed files with 285 additions and 15 deletions.
10 changes: 10 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ func (api *api) OpenChannel(ctx context.Context, openChannelRequest *OpenChannel
return api.svc.GetLNClient().OpenChannel(ctx, openChannelRequest)
}

func (api *api) DisconnectPeer(ctx context.Context, peerId string) error {
if api.svc.GetLNClient() == nil {
return errors.New("LNClient not started")
}
api.logger.WithFields(logrus.Fields{
"peer_id": peerId,
}).Info("Disconnecting peer")
return api.svc.GetLNClient().DisconnectPeer(ctx, peerId)
}

func (api *api) CloseChannel(ctx context.Context, peerId, channelId string, force bool) (*CloseChannelResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
Expand Down
1 change: 1 addition & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type API interface {
GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error)
ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error)
ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error
DisconnectPeer(ctx context.Context, peerId string) error
OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error)
CloseChannel(ctx context.Context, peerId, channelId string, force bool) (*CloseChannelResponse, error)
GetNewOnchainAddress(ctx context.Context) (*NewOnchainAddressResponse, error)
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { Intro } from "src/screens/Intro";
import AlbyAuthRedirect from "src/screens/alby/AlbyAuthRedirect";
import { CurrentChannelOrder } from "src/screens/channels/CurrentChannelOrder";
import { Success } from "src/screens/onboarding/Success";
import Peers from "src/screens/peers/Peers";
import { ChangeUnlockPassword } from "src/screens/settings/ChangeUnlockPassword";
import DebugTools from "src/screens/settings/DebugTools";
import { RestoreNode } from "src/screens/setup/RestoreNode";
Expand Down Expand Up @@ -96,6 +97,7 @@ function App() {
/>
</Route>
<Route path="peers" element={<DefaultRedirect />}>
<Route index element={<Peers />} />
<Route path="new" element={<ConnectPeer />} />
</Route>
</Route>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/hooks/usePeers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import useSWR, { SWRConfiguration } from "swr";

import { Peer } from "src/types";
import { swrFetcher } from "src/utils/swr";

const pollConfiguration: SWRConfiguration = {
refreshInterval: 3000,
};

export function usePeers(poll = false) {
return useSWR<Peer[]>(
"/api/peers",
swrFetcher,
poll ? pollConfiguration : undefined
);
}
30 changes: 15 additions & 15 deletions frontend/src/screens/channels/Channels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Unplug,
} from "lucide-react";
import React from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import AppHeader from "src/components/AppHeader.tsx";
import EmptyState from "src/components/EmptyState.tsx";
import Loading from "src/components/Loading.tsx";
Expand Down Expand Up @@ -64,16 +64,9 @@ export default function Channels() {
const [nodes, setNodes] = React.useState<Node[]>([]);
const { data: info, mutate: reloadInfo } = useInfo();
const { data: csrf } = useCSRF();
const navigate = useNavigate();
const redeemOnchainFunds = useRedeemOnchainFunds();

React.useEffect(() => {
if (!info || info.running) {
return;
}
navigate("/");
}, [info, navigate]);

// TODO: move to NWC backend
const loadNodeStats = React.useCallback(async () => {
if (!channels) {
return [];
Expand Down Expand Up @@ -243,14 +236,15 @@ export default function Channels() {
<DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link to="/channels/onchain/new-address">
<Link to="/channels/onchain/new-address" className="w-full">
On-Chain Address
</Link>
</DropdownMenuItem>
{(balances?.onchain.spendable || 0) > ONCHAIN_DUST_SATS && (
<DropdownMenuItem
onClick={redeemOnchainFunds.redeemFunds}
disabled={redeemOnchainFunds.isLoading}
className="w-full cursor-pointer"
>
Redeem Onchain Funds
{redeemOnchainFunds.isLoading && <Loading />}
Expand All @@ -263,12 +257,19 @@ export default function Channels() {
<DropdownMenuGroup>
<DropdownMenuLabel>Management</DropdownMenuLabel>
<DropdownMenuItem>
<Link to="/peers/new">Connect Peer</Link>
<Link className="w-full" to="/peers">
Connected Peers
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link to="/wallet/sign-message">Sign Message</Link>
<Link className="w-full" to="/wallet/sign-message">
Sign Message
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={resetRouter}>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={resetRouter}
>
Clear Routing Data
</DropdownMenuItem>
</DropdownMenuGroup>
Expand Down Expand Up @@ -442,8 +443,7 @@ export default function Channels() {
rel="noopener noreferer"
>
<Button variant="link" className="p-0 mr-2">
{alias ||
channel.remotePubkey.substring(0, 5) + "..."}
{alias}
</Button>
</a>
<Badge variant="outline">
Expand Down
177 changes: 177 additions & 0 deletions frontend/src/screens/peers/Peers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { MoreHorizontal, Trash2 } from "lucide-react";
import React from "react";
import { Link } from "react-router-dom";
import AppHeader from "src/components/AppHeader.tsx";
import { Badge } from "src/components/ui/badge.tsx";
import { Button } from "src/components/ui/button.tsx";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "src/components/ui/dropdown-menu.tsx";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "src/components/ui/table.tsx";
import { toast } from "src/components/ui/use-toast.ts";
import { useChannels } from "src/hooks/useChannels";
import { usePeers } from "src/hooks/usePeers.ts";
import { useSyncWallet } from "src/hooks/useSyncWallet.ts";
import { Node } from "src/types";
import { request } from "src/utils/request";
import { useCSRF } from "../../hooks/useCSRF.ts";

export default function Peers() {
useSyncWallet();
const { data: peers, mutate: reloadPeers } = usePeers();
const { data: channels } = useChannels();
const [nodes, setNodes] = React.useState<Node[]>([]);
const { data: csrf } = useCSRF();

// TODO: move to NWC backend
const loadNodeStats = React.useCallback(async () => {
if (!peers) {
return [];
}
const nodes = await Promise.all(
peers?.map(async (peer): Promise<Node | undefined> => {
try {
const response = await request<Node>(
`/api/mempool?endpoint=/v1/lightning/nodes/${peer.nodeId}`
);
return response;
} catch (error) {
console.error(error);
return undefined;
}
})
);
setNodes(nodes.filter((node) => !!node) as Node[]);
}, [peers]);

React.useEffect(() => {
loadNodeStats();
}, [loadNodeStats]);

async function disconnectPeer(peerId: string) {
try {
if (!csrf) {
throw new Error("csrf not loaded");
}
if (!peerId) {
throw new Error("peer missing");
}
if (!channels) {
throw new Error("channels not loaded");
}
if (channels.some((channel) => channel.remotePubkey === peerId)) {
throw new Error("you have one or more open channels with " + peerId);
}
if (
!confirm(
"Are you sure you wish to disconnect with peer " + peerId + "?"
)
) {
return;
}
console.log(`Disconnecting from ${peerId}`);

await request(`/api/peers/${peerId}`, {
method: "DELETE",
headers: {
"X-CSRF-Token": csrf,
},
});
toast({ title: "Successfully disconnected from peer " + peerId });
await reloadPeers();
} catch (e) {
toast({
variant: "destructive",
title: "Failed to disconnect peer: " + e,
});
console.error(e);
}
}

return (
<>
<AppHeader
title="Peers"
description="Manage your connections with other lightning nodes"
contentRight={
<>
<Link to="/peers/new">
<Button>Connect Peer</Button>
</Link>
</>
}
></AppHeader>

<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[80px]">Status</TableHead>
<TableHead>Node</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<>
{peers?.map((peer) => {
const node = nodes.find((n) => n.public_key === peer.nodeId);
const alias = node?.alias || "Unknown";

return (
<TableRow key={peer.nodeId}>
<TableCell>
{peer.isConnected ? (
<Badge>Online</Badge>
) : (
<Badge variant="outline">Offline</Badge>
)}{" "}
</TableCell>
<TableCell className="flex flex-row items-center">
<a
title={peer.nodeId}
href={`https://amboss.space/node/${peer.nodeId}`}
target="_blank"
rel="noopener noreferer"
>
<Button variant="link" className="p-0 mr-2">
{alias}
</Button>
{peer.nodeId}
</a>
</TableCell>
<TableCell>
<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"
onClick={() => disconnectPeer(peer.nodeId)}
>
<Trash2 className="text-destructive" />
Disconnect Peer
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</>
</TableBody>
</Table>
</>
);
}
7 changes: 7 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ export type Channel = {
confirmationsRequired?: number;
};

export type Peer = {
nodeId: string;
address: string;
isPersisted: boolean;
isConnected: boolean;
};

export type NodeConnectionInfo = {
pubkey: string;
address: string;
Expand Down
15 changes: 15 additions & 0 deletions http/http_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
e.GET("/api/node/network-graph", httpSvc.nodeNetworkGraphHandler, authMiddleware)
e.GET("/api/peers", httpSvc.listPeers, authMiddleware)
e.POST("/api/peers", httpSvc.connectPeerHandler, authMiddleware)
e.DELETE("/api/peers/:peerId", httpSvc.disconnectPeerHandler, authMiddleware)
e.DELETE("/api/peers/:peerId/channels/:channelId", httpSvc.closeChannelHandler, authMiddleware)
e.POST("/api/wallet/new-address", httpSvc.newOnchainAddressHandler, authMiddleware)
e.POST("/api/wallet/redeem-onchain-funds", httpSvc.redeemOnchainFundsHandler, authMiddleware)
Expand Down Expand Up @@ -472,6 +473,20 @@ func (httpSvc *HttpService) openChannelHandler(c echo.Context) error {
return c.JSON(http.StatusOK, openChannelResponse)
}

func (httpSvc *HttpService) disconnectPeerHandler(c echo.Context) error {
ctx := c.Request().Context()

err := httpSvc.api.DisconnectPeer(ctx, c.Param("peerId"))

if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to disconnect peer: %s", err.Error()),
})
}

return c.NoContent(http.StatusNoContent)
}

func (httpSvc *HttpService) closeChannelHandler(c echo.Context) error {
ctx := c.Request().Context()

Expand Down
4 changes: 4 additions & 0 deletions lnclient/breez/breez.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,7 @@ func (bs *BreezService) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraph
}

func (bs *BreezService) UpdateLastWalletSyncRequest() {}

func (bs *BreezService) DisconnectPeer(ctx context.Context, peerId string) error {
return nil
}
4 changes: 4 additions & 0 deletions lnclient/greenlight/greenlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,7 @@ func (gs *GreenlightService) GetNetworkGraph(nodeIds []string) (lnclient.Network
}

func (gs *GreenlightService) UpdateLastWalletSyncRequest() {}

func (gs *GreenlightService) DisconnectPeer(ctx context.Context, peerId string) error {
return nil
}
4 changes: 4 additions & 0 deletions lnclient/ldk/ldk.go
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,10 @@ func (ls *LDKService) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient.N
}, nil
}

func (ls *LDKService) DisconnectPeer(ctx context.Context, peerId string) error {
return ls.node.Disconnect(peerId)
}

func (ls *LDKService) UpdateLastWalletSyncRequest() {
ls.lastWalletSyncRequest = time.Now()
}
4 changes: 4 additions & 0 deletions lnclient/lnd/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,7 @@ func (svc *LNDService) GetNetworkGraph(nodeIds []string) (lnclient.NetworkGraphR
}

func (svc *LNDService) UpdateLastWalletSyncRequest() {}

func (svc *LNDService) DisconnectPeer(ctx context.Context, peerId string) error {
return nil
}
1 change: 1 addition & 0 deletions lnclient/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type LNClient interface {
ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error
OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error)
CloseChannel(ctx context.Context, closeChannelRequest *CloseChannelRequest) (*CloseChannelResponse, error)
DisconnectPeer(ctx context.Context, peerId string) error
GetNewOnchainAddress(ctx context.Context) (string, error)
ResetRouter(key string) error
GetOnchainBalance(ctx context.Context) (*OnchainBalanceResponse, error)
Expand Down
Loading

0 comments on commit 16dc19e

Please sign in to comment.