peers and channels details / actions

This commit is contained in:
Paul Miller
2023-04-20 10:02:22 -05:00
parent ca0cc8485d
commit 5ddd7cb277
4 changed files with 121 additions and 24 deletions

View File

@@ -1,15 +1,50 @@
import { useMegaStore } from "~/state/megaStore";
import { Card, Hr, SmallHeader, Button, InnerCard } from "~/components/layout";
import { Card, Hr, SmallHeader, Button, InnerCard, VStack } from "~/components/layout";
import PeerConnectModal from "~/components/PeerConnectModal";
import { For, Show, Suspense, createResource, createSignal } from "solid-js";
import { For, Show, Suspense, createEffect, createResource, createSignal, onCleanup } from "solid-js";
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
import { TextField } from "@kobalte/core";
import { Collapsible, TextField, toaster } from "@kobalte/core";
import mempoolTxUrl from "~/utils/mempoolTxUrl";
import eify from "~/utils/eify";
import { ConfirmDialog } from "./Dialog";
import { ToastItem, showToast } from "./Toaster";
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
type RefetchPeersType = (info?: unknown) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined
function PeerItem(props: { peer: MutinyPeer }) {
const [state, _] = useMegaStore()
const handleDisconnectPeer = async () => {
const nodes = await state.node_manager?.list_nodes();
const firstNode = nodes[0] as string || ""
if (props.peer.is_connected) {
await state.node_manager?.disconnect_peer(firstNode, props.peer.pubkey);
} else {
await state.node_manager?.delete_peer(firstNode, props.peer.pubkey);
}
};
return (
<Collapsible.Root>
<Collapsible.Trigger class="w-full">
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
{">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey}
</h2>
</Collapsible.Trigger>
<Collapsible.Content>
<VStack>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(props.peer, null, 2)}
</pre>
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>Disconnect</Button>
</VStack>
</Collapsible.Content>
</Collapsible.Root>
)
}
function PeersList() {
const [state, _] = useMegaStore()
@@ -19,6 +54,16 @@ function PeersList() {
const [peers, { refetch }] = createResource(getPeers);
createEffect(() => {
// refetch peers every 5 seconds
const interval = setTimeout(() => {
refetch();
}, 5000);
onCleanup(() => {
clearInterval(interval);
});
})
return (
<>
<SmallHeader>
@@ -26,13 +71,13 @@ function PeersList() {
</SmallHeader>
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
<Suspense>
<For each={peers()} fallback={<code>No peers</code>}>
{(peer) => (
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(peer, null, 2)}
</pre>
)}
</For>
<VStack>
<For each={peers()} fallback={<code>No peers</code>}>
{(peer) => (
<PeerItem peer={peer} />
)}
</For>
</VStack>
</Suspense>
<Button layout="small" onClick={refetch}>Refresh Peers</Button>
<ConnectPeer refetchPeers={refetch} />
@@ -81,6 +126,54 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
type RefetchChannelsListType = (info?: unknown) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | null | undefined
function ChannelItem(props: { channel: MutinyChannel, network?: string }) {
const [state, _] = useMegaStore()
const [confirmOpen, setConfirmOpen] = createSignal(false);
const [confirmLoading, setConfirmLoading] = createSignal(false);
function handleCloseChannel() {
setConfirmOpen(true);
}
async function confirmCloseChannel() {
setConfirmLoading(true);
try {
await state.node_manager?.close_channel(props.channel.outpoint as string)
} catch (e) {
console.error(e);
showToast(eify(e));
}
setConfirmLoading(false);
setConfirmOpen(false);
}
return (
<Collapsible.Root>
<Collapsible.Trigger class="w-full">
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
{">"} {props.channel.peer}
</h2>
</Collapsible.Trigger>
<Collapsible.Content>
<VStack>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(props.channel, null, 2)}
</pre>
<a class="" href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)} target="_blank" rel="noreferrer">
Mempool Link
</a>
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>Close Channel</Button>
</VStack>
<ConfirmDialog isOpen={confirmOpen()} onConfirm={confirmCloseChannel} onCancel={() => setConfirmOpen(false)} loading={confirmLoading()}>
<p>Are you sure you want to close this channel?</p>
</ConfirmDialog>
</Collapsible.Content>
</Collapsible.Root>
)
}
function ChannelsList() {
const [state, _] = useMegaStore()
@@ -90,6 +183,16 @@ function ChannelsList() {
const [channels, { refetch }] = createResource(getChannels);
createEffect(() => {
// refetch channels every 5 seconds
const interval = setTimeout(() => {
refetch();
}, 5000);
onCleanup(() => {
clearInterval(interval);
});
})
const network = state.node_manager?.get_network();
return (
@@ -101,14 +204,7 @@ function ChannelsList() {
<Suspense>
<For each={channels()} fallback={<code>No channels</code>}>
{(channel) => (
<>
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(channel, null, 2)}
</pre>
<a class="text-sm font-light opacity-50 mt-2" href={mempoolTxUrl(channel.outpoint?.split(":")[0], network)} target="_blank" rel="noreferrer">
Mempool Link
</a>
</>
<ChannelItem channel={channel} network={network} />
)}
</For>

View File

@@ -1,5 +1,6 @@
import { Dialog } from "@kobalte/core";
import { ButtonLink, SmallHeader } from "~/components/layout";
import close from "~/assets/icons/close.svg";
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
@@ -12,14 +13,14 @@ export function SentModal(props: { details?: { nice: string } }) {
<Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>
<div class="flex justify-between mb-2">
<div class="flex justify-between mb-2 items-center">
<Dialog.Title>
<SmallHeader>
Sent!
</SmallHeader>
</Dialog.Title>
<Dialog.CloseButton class="dialog__close-button">
<code>X</code>
<Dialog.CloseButton class="p-2 hover:bg-white/10 rounded-lg active:bg-m-blue">
<img src={close} alt="Close" />
</Dialog.CloseButton>
</div>
<Dialog.Description class="flex flex-col gap-4">

View File

@@ -10,7 +10,7 @@ const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
const Card: ParentComponent<{ title?: string }> = (props) => {
return (
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950'>
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
{props.title && <SmallHeader>{props.title}</SmallHeader>}
{props.children}
</div>

View File

@@ -229,7 +229,7 @@ export default function Receive() {
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
<ReceiveSuccessModal title="Payment Received!" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
<div class="flex flex-col items-center gap-8">
<img src={party} alt="party" class="w-1/2 mx-auto" />
<img src={party} alt="party" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat />
</div>
</ReceiveSuccessModal>
@@ -237,7 +237,7 @@ export default function Receive() {
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
<ReceiveSuccessModal title="Payment Received!" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
<div class="flex flex-col items-center gap-8">
<img src={party} alt="party" class="w-1/2 mx-auto" />
<img src={party} alt="party" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
<Amount amountSats={paymentTx()?.received} showFiat />
<a href={mempoolTxUrl(paymentTx()?.txid, "signet")} target="_blank" rel="noreferrer">
Mempool Link