mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-17 06:14:21 +01:00
list individual channels on channels page
This commit is contained in:
committed by
Tony Giorgio
parent
d61f845e2e
commit
f1e80cc475
@@ -315,7 +315,12 @@ export default {
|
|||||||
reserve_tip:
|
reserve_tip:
|
||||||
"About 1% of your channel balance is reserved on lightning for fees. Additional reserves are required for channels you opened via swap.",
|
"About 1% of your channel balance is reserved on lightning for fees. Additional reserves are required for channels you opened via swap.",
|
||||||
no_channels:
|
no_channels:
|
||||||
"It looks like you don't have any channels yet. To get started, receive some sats over lightning, or swap some on-chain funds into a channel. Get your hands dirty!"
|
"It looks like you don't have any channels yet. To get started, receive some sats over lightning, or swap some on-chain funds into a channel. Get your hands dirty!",
|
||||||
|
close_channel: "Close",
|
||||||
|
online_channels: "Online Channels",
|
||||||
|
offline_channels: "Offline Channels",
|
||||||
|
close_channel_confirm:
|
||||||
|
"Closing this channel will move the balance on-chain and incur an on-chain fee."
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
title: "Wallet Connections",
|
title: "Wallet Connections",
|
||||||
|
|||||||
@@ -1,37 +1,62 @@
|
|||||||
import { createResource, Match, Switch } from "solid-js";
|
import { MutinyChannel } from "@mutinywallet/mutiny-wasm";
|
||||||
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
Match,
|
||||||
|
Show,
|
||||||
|
Suspense,
|
||||||
|
Switch
|
||||||
|
} from "solid-js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AmountSmall,
|
AmountSmall,
|
||||||
BackLink,
|
BackLink,
|
||||||
Card,
|
Card,
|
||||||
|
Collapser,
|
||||||
|
ConfirmDialog,
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
|
ExternalLink,
|
||||||
LargeHeader,
|
LargeHeader,
|
||||||
MutinyWalletGuard,
|
MutinyWalletGuard,
|
||||||
NavBar,
|
NavBar,
|
||||||
NiceP,
|
NiceP,
|
||||||
SafeArea,
|
SafeArea,
|
||||||
|
SettingsCard,
|
||||||
|
showToast,
|
||||||
SmallHeader,
|
SmallHeader,
|
||||||
TinyText,
|
TinyText,
|
||||||
VStack
|
VStack
|
||||||
} from "~/components";
|
} from "~/components";
|
||||||
import { useI18n } from "~/i18n/context";
|
import { useI18n } from "~/i18n/context";
|
||||||
|
import { Network } from "~/logic/mutinyWalletSetup";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
import { createDeepSignal, eify, mempoolTxUrl } from "~/utils";
|
||||||
|
|
||||||
export function BalanceBar(props: {
|
export function BalanceBar(props: {
|
||||||
inbound: number;
|
inbound: number;
|
||||||
reserve: number;
|
reserve: number;
|
||||||
outbound: number;
|
outbound: number;
|
||||||
|
hideHeader?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<VStack smallgap>
|
<VStack smallgap>
|
||||||
<div class="flex justify-between">
|
<Show when={!props.hideHeader}>
|
||||||
<SmallHeader>
|
<div class="flex justify-between">
|
||||||
{i18n.t("settings.channels.outbound")}
|
<SmallHeader>
|
||||||
</SmallHeader>
|
{i18n.t("settings.channels.outbound")}
|
||||||
<SmallHeader>{i18n.t("settings.channels.reserve")}</SmallHeader>
|
</SmallHeader>
|
||||||
<SmallHeader>{i18n.t("settings.channels.inbound")}</SmallHeader>
|
<SmallHeader>
|
||||||
</div>
|
{i18n.t("settings.channels.reserve")}
|
||||||
|
</SmallHeader>
|
||||||
|
<SmallHeader>
|
||||||
|
{i18n.t("settings.channels.inbound")}
|
||||||
|
</SmallHeader>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<div class="flex w-full gap-1">
|
<div class="flex w-full gap-1">
|
||||||
<div
|
<div
|
||||||
class="min-w-fit rounded-l-xl bg-m-green p-2"
|
class="min-w-fit rounded-l-xl bg-m-green p-2"
|
||||||
@@ -62,13 +87,105 @@ export function BalanceBar(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitChannelNumbers(channel: MutinyChannel): {
|
||||||
|
inbound: number;
|
||||||
|
reserve: number;
|
||||||
|
outbound: number;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
inbound: Number(channel.inbound) || 0,
|
||||||
|
reserve: Number(channel.reserve),
|
||||||
|
outbound: Number(channel.balance)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function SingleChannelItem(props: { channel: MutinyChannel }) {
|
||||||
|
const i18n = useI18n();
|
||||||
|
const [state, _actions] = useMegaStore();
|
||||||
|
const network = state.mutiny_wallet?.get_network() as Network;
|
||||||
|
|
||||||
|
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||||
|
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
||||||
|
|
||||||
|
function confirmChannelClose() {
|
||||||
|
setConfirmOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeChannel() {
|
||||||
|
try {
|
||||||
|
if (!props.channel.outpoint) return;
|
||||||
|
setConfirmLoading(true);
|
||||||
|
await state.mutiny_wallet?.close_channel(
|
||||||
|
props.channel.outpoint,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast(eify(e));
|
||||||
|
} finally {
|
||||||
|
setConfirmOpen(false);
|
||||||
|
setConfirmLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelDetails = createMemo(() => splitChannelNumbers(props.channel));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<VStack smallgap>
|
||||||
|
<BalanceBar
|
||||||
|
inbound={channelDetails().inbound}
|
||||||
|
reserve={channelDetails().reserve}
|
||||||
|
outbound={channelDetails().outbound}
|
||||||
|
hideHeader
|
||||||
|
/>
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<ExternalLink
|
||||||
|
href={mempoolTxUrl(
|
||||||
|
props.channel.outpoint?.split(":")[0],
|
||||||
|
network
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n.t("common.view_transaction")}
|
||||||
|
</ExternalLink>
|
||||||
|
<button
|
||||||
|
onClick={confirmChannelClose}
|
||||||
|
class="self-center font-semibold text-m-red no-underline active:text-m-red/80"
|
||||||
|
>
|
||||||
|
{i18n.t("settings.channels.close_channel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
loading={confirmLoading()}
|
||||||
|
open={confirmOpen()}
|
||||||
|
onConfirm={closeChannel}
|
||||||
|
onCancel={() => setConfirmOpen(false)}
|
||||||
|
>
|
||||||
|
{i18n.t("settings.channels.close_channel_confirm")}
|
||||||
|
</ConfirmDialog>
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function LiquidityMonitor() {
|
export function LiquidityMonitor() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const [channelInfo] = createResource(async () => {
|
async function listChannels() {
|
||||||
try {
|
try {
|
||||||
const channels = await state.mutiny_wallet?.list_channels();
|
const channels: MutinyChannel[] | undefined =
|
||||||
|
await state.mutiny_wallet?.list_channels();
|
||||||
|
|
||||||
|
if (!channels)
|
||||||
|
return {
|
||||||
|
inbound: 0,
|
||||||
|
reserve: 0,
|
||||||
|
outbound: 0,
|
||||||
|
channelCount: 0
|
||||||
|
};
|
||||||
|
|
||||||
let outbound = 0n;
|
let outbound = 0n;
|
||||||
let inbound = 0n;
|
let inbound = 0n;
|
||||||
let reserve = 0n;
|
let reserve = 0n;
|
||||||
@@ -83,37 +200,94 @@ export function LiquidityMonitor() {
|
|||||||
inbound,
|
inbound,
|
||||||
reserve,
|
reserve,
|
||||||
outbound,
|
outbound,
|
||||||
channelCount: channels?.length
|
channelCount: channels?.length,
|
||||||
|
online: channels?.filter((c) => c.is_usable),
|
||||||
|
offline: channels?.filter((c) => !c.is_usable)
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return { inbound: 0, reserve: 0, outbound: 0, channelCount: 0 };
|
return { inbound: 0, reserve: 0, outbound: 0, channelCount: 0 };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [channelInfo, { refetch }] = createResource(listChannels, {
|
||||||
|
storage: createDeepSignal
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
// Refetch on the sync interval
|
||||||
|
if (!state.is_syncing) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={channelInfo()?.channelCount}>
|
<Match when={channelInfo()?.channelCount}>
|
||||||
<Card>
|
<VStack>
|
||||||
<NiceP>
|
<Card>
|
||||||
{i18n.t("settings.channels.have_channels")}{" "}
|
<NiceP>
|
||||||
{channelInfo()?.channelCount}{" "}
|
{i18n.t("settings.channels.have_channels")}{" "}
|
||||||
{channelInfo()?.channelCount === 1
|
{channelInfo()?.channelCount}{" "}
|
||||||
? i18n.t("settings.channels.have_channels_one")
|
{channelInfo()?.channelCount === 1
|
||||||
: i18n.t("settings.channels.have_channels_many")}
|
? i18n.t("settings.channels.have_channels_one")
|
||||||
</NiceP>{" "}
|
: i18n.t(
|
||||||
<BalanceBar
|
"settings.channels.have_channels_many"
|
||||||
inbound={Number(channelInfo()?.inbound) || 0}
|
)}
|
||||||
reserve={Number(channelInfo()?.reserve) || 0}
|
</NiceP>{" "}
|
||||||
outbound={Number(channelInfo()?.outbound) || 0}
|
<BalanceBar
|
||||||
/>
|
inbound={Number(channelInfo()?.inbound) || 0}
|
||||||
<TinyText>
|
reserve={Number(channelInfo()?.reserve) || 0}
|
||||||
{i18n.t("settings.channels.inbound_outbound_tip")}
|
outbound={Number(channelInfo()?.outbound) || 0}
|
||||||
</TinyText>
|
/>
|
||||||
<TinyText>
|
<TinyText>
|
||||||
{i18n.t("settings.channels.reserve_tip")}
|
{i18n.t("settings.channels.inbound_outbound_tip")}
|
||||||
</TinyText>
|
</TinyText>
|
||||||
</Card>
|
<TinyText>
|
||||||
|
{i18n.t("settings.channels.reserve_tip")}
|
||||||
|
</TinyText>
|
||||||
|
</Card>
|
||||||
|
<Show when={channelInfo()?.online?.length}>
|
||||||
|
<SettingsCard>
|
||||||
|
<Collapser
|
||||||
|
title={i18n.t(
|
||||||
|
"settings.channels.online_channels"
|
||||||
|
)}
|
||||||
|
activityLight="on"
|
||||||
|
>
|
||||||
|
<VStack>
|
||||||
|
<For each={channelInfo()?.online}>
|
||||||
|
{(channel) => (
|
||||||
|
<SingleChannelItem
|
||||||
|
channel={channel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</VStack>
|
||||||
|
</Collapser>
|
||||||
|
</SettingsCard>
|
||||||
|
</Show>
|
||||||
|
<Show when={channelInfo()?.offline?.length}>
|
||||||
|
<SettingsCard>
|
||||||
|
<Collapser
|
||||||
|
title={i18n.t(
|
||||||
|
"settings.channels.offline_channels"
|
||||||
|
)}
|
||||||
|
activityLight="off"
|
||||||
|
>
|
||||||
|
<VStack>
|
||||||
|
<For each={channelInfo()?.offline}>
|
||||||
|
{(channel) => (
|
||||||
|
<SingleChannelItem
|
||||||
|
channel={channel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</VStack>
|
||||||
|
</Collapser>
|
||||||
|
</SettingsCard>
|
||||||
|
</Show>
|
||||||
|
</VStack>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<NiceP>{i18n.t("settings.channels.no_channels")}</NiceP>
|
<NiceP>{i18n.t("settings.channels.no_channels")}</NiceP>
|
||||||
@@ -135,7 +309,9 @@ export function Channels() {
|
|||||||
<LargeHeader>
|
<LargeHeader>
|
||||||
{i18n.t("settings.channels.title")}
|
{i18n.t("settings.channels.title")}
|
||||||
</LargeHeader>
|
</LargeHeader>
|
||||||
<LiquidityMonitor />
|
<Suspense>
|
||||||
|
<LiquidityMonitor />
|
||||||
|
</Suspense>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="settings" />
|
<NavBar activeTab="settings" />
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
|
|||||||
Reference in New Issue
Block a user