mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 23:04:25 +01:00
clean up settings and debug screens
This commit is contained in:
@@ -19,7 +19,7 @@ import chain from "~/assets/icons/chain-black.svg";
|
||||
import copyIcon from "~/assets/icons/copy.svg";
|
||||
|
||||
import { ActivityAmount, HackActivityType } from "./ActivityItem";
|
||||
import { CopyButton } from "./ShareCard";
|
||||
import { CopyButton, TruncateMiddle } from "./ShareCard";
|
||||
import { prettyPrintTime } from "~/utils/prettyPrintTime";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { tagToMutinyTag } from "~/utils/tags";
|
||||
@@ -153,13 +153,18 @@ const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
function MiniStringShower(props: { text: string }) {
|
||||
const [copy, _copied] = useCopy({ copiedTimeout: 1000 });
|
||||
export function MiniStringShower(props: { text: string }) {
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
return (
|
||||
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
|
||||
<pre class="truncate text-neutral-300 font-light">{props.text}</pre>
|
||||
<button class="w-[1rem]" onClick={() => copy(props.text)}>
|
||||
<TruncateMiddle text={props.text} />
|
||||
{/* <pre class="truncate text-neutral-300 font-light">{props.text}</pre> */}
|
||||
<button
|
||||
class="w-[1.5rem] p-1"
|
||||
classList={{ "bg-m-green rounded": copied() }}
|
||||
onClick={() => copy(props.text)}
|
||||
>
|
||||
<img src={copyIcon} alt="copy" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Button, InnerCard, VStack } from "~/components/layout";
|
||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||
import { createSignal } from "solid-js";
|
||||
import eify from "~/utils/eify";
|
||||
import { showToast } from "./Toaster";
|
||||
@@ -72,10 +72,23 @@ export function ImportExport() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<InnerCard>
|
||||
<InnerCard title="Export wallet state">
|
||||
<NiceP>
|
||||
You can export your entire Mutiny Wallet state to a file and
|
||||
import it into a new browser. It usually works!
|
||||
</NiceP>
|
||||
<NiceP>
|
||||
<strong class="font-semibold">Important caveats:</strong>{" "}
|
||||
after exporting don't do any operations in the original
|
||||
browser. If you do, you'll need to export again. After a
|
||||
successful import, a best practice is to clear the state of
|
||||
the original browser just to make sure you don't create
|
||||
conflicts.
|
||||
</NiceP>
|
||||
<div />
|
||||
<VStack>
|
||||
<Button onClick={handleSave}>Save State As File</Button>
|
||||
<Button onClick={uploadFile}>Upload Saved State</Button>
|
||||
<Button onClick={uploadFile}>Import State From File</Button>
|
||||
</VStack>
|
||||
</InnerCard>
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import {
|
||||
Card,
|
||||
Hr,
|
||||
SmallHeader,
|
||||
Button,
|
||||
InnerCard,
|
||||
VStack
|
||||
} from "~/components/layout";
|
||||
import PeerConnectModal from "~/components/PeerConnectModal";
|
||||
import { Hr, Button, InnerCard, VStack } from "~/components/layout";
|
||||
import NostrWalletConnectModal from "~/components/NostrWalletConnectModal";
|
||||
import {
|
||||
For,
|
||||
Show,
|
||||
Suspense,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
onCleanup
|
||||
} from "solid-js";
|
||||
import { For, Show, Suspense, createResource, createSignal } from "solid-js";
|
||||
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
|
||||
import { Collapsible, TextField } from "@kobalte/core";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
@@ -27,6 +11,9 @@ import { showToast } from "~/components/Toaster";
|
||||
import { ImportExport } from "~/components/ImportExport";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { ExternalLink } from "./layout/ExternalLink";
|
||||
import { Logs } from "./Logs";
|
||||
import { Restart } from "./Restart";
|
||||
import { MiniStringShower } from "./DetailsModal";
|
||||
|
||||
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
|
||||
type RefetchPeersType = (
|
||||
@@ -90,30 +77,24 @@ function PeersList() {
|
||||
|
||||
const [peers, { refetch }] = createResource(getPeers);
|
||||
|
||||
createEffect(() => {
|
||||
// refetch peers every 5 seconds
|
||||
const interval = setTimeout(() => {
|
||||
refetch();
|
||||
}, 5000);
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SmallHeader>Peers</SmallHeader>
|
||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||
<Suspense>
|
||||
<VStack>
|
||||
<For each={peers()} fallback={<code>No peers</code>}>
|
||||
{(peer) => <PeerItem peer={peer} />}
|
||||
</For>
|
||||
</VStack>
|
||||
</Suspense>
|
||||
<Button layout="small" onClick={refetch}>
|
||||
Refresh Peers
|
||||
</Button>
|
||||
<InnerCard title="Peers">
|
||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||
<Suspense>
|
||||
<VStack>
|
||||
<For
|
||||
each={peers.latest}
|
||||
fallback={<code>No peers</code>}
|
||||
>
|
||||
{(peer) => <PeerItem peer={peer} />}
|
||||
</For>
|
||||
</VStack>
|
||||
</Suspense>
|
||||
<Button layout="small" onClick={refetch}>
|
||||
Refresh Peers
|
||||
</Button>
|
||||
</InnerCard>
|
||||
<ConnectPeer refetchPeers={refetch} />
|
||||
</>
|
||||
);
|
||||
@@ -155,10 +136,10 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||
</TextField.Label>
|
||||
<TextField.Input
|
||||
class="w-full p-2 rounded-lg text-black"
|
||||
placeholder="mutiny:028241..."
|
||||
placeholder="028241..."
|
||||
/>
|
||||
<TextField.ErrorMessage class="text-red-500">
|
||||
Expecting something like mutiny:abc123...
|
||||
Expecting a value...
|
||||
</TextField.ErrorMessage>
|
||||
</TextField.Root>
|
||||
<Button layout="small" type="submit">
|
||||
@@ -249,39 +230,30 @@ function ChannelsList() {
|
||||
|
||||
const [channels, { refetch }] = createResource(getChannels);
|
||||
|
||||
createEffect(() => {
|
||||
// refetch channels every 5 seconds
|
||||
const interval = setTimeout(() => {
|
||||
refetch();
|
||||
}, 5000);
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SmallHeader>Channels</SmallHeader>
|
||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||
<Suspense>
|
||||
<For each={channels()} fallback={<code>No channels</code>}>
|
||||
{(channel) => (
|
||||
<ChannelItem channel={channel} network={network} />
|
||||
)}
|
||||
</For>
|
||||
</Suspense>
|
||||
<Button
|
||||
type="button"
|
||||
layout="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
Refresh Channels
|
||||
</Button>
|
||||
<InnerCard title="Channels">
|
||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||
<Suspense>
|
||||
<For each={channels()} fallback={<code>No channels</code>}>
|
||||
{(channel) => (
|
||||
<ChannelItem channel={channel} network={network} />
|
||||
)}
|
||||
</For>
|
||||
</Suspense>
|
||||
<Button
|
||||
type="button"
|
||||
layout="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
Refresh Channels
|
||||
</Button>
|
||||
</InnerCard>
|
||||
<OpenChannel refetchChannels={refetch} />
|
||||
</>
|
||||
);
|
||||
@@ -429,33 +401,33 @@ function LnUrlAuth() {
|
||||
);
|
||||
}
|
||||
|
||||
function ListTags() {
|
||||
const [_state, actions] = useMegaStore();
|
||||
function ListNodes() {
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const [tags] = createResource(actions.listTags);
|
||||
const getNodeIds = async () => {
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
return nodes as string[];
|
||||
};
|
||||
|
||||
const [nodeIds] = createResource(getNodeIds);
|
||||
|
||||
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">
|
||||
{">"} Tags
|
||||
</h2>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<VStack>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(tags(), null, 2)}
|
||||
</pre>
|
||||
</VStack>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
<InnerCard title="Nodes">
|
||||
<Suspense>
|
||||
<For each={nodeIds()} fallback={<code>No nodes</code>}>
|
||||
{(nodeId) => <MiniStringShower text={nodeId} />}
|
||||
</For>
|
||||
</Suspense>
|
||||
</InnerCard>
|
||||
);
|
||||
}
|
||||
|
||||
export default function KitchenSink() {
|
||||
return (
|
||||
<Card title="Kitchen Sink">
|
||||
<PeerConnectModal />
|
||||
<>
|
||||
<Logs />
|
||||
<Hr />
|
||||
<ListNodes />
|
||||
<Hr />
|
||||
<NostrWalletConnectModal />
|
||||
<Hr />
|
||||
@@ -465,10 +437,10 @@ export default function KitchenSink() {
|
||||
<Hr />
|
||||
<LnUrlAuth />
|
||||
<Hr />
|
||||
<ListTags />
|
||||
|
||||
<Restart />
|
||||
<Hr />
|
||||
<ImportExport />
|
||||
</Card>
|
||||
<Hr />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
72
src/components/LiquidityMonitor.tsx
Normal file
72
src/components/LiquidityMonitor.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Show, createResource } from "solid-js";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Card, NiceP, SmallHeader, TinyText, VStack } from "./layout";
|
||||
import { AmountSmall } from "./Amount";
|
||||
|
||||
function BalanceBar(props: { inbound: number; outbound: number }) {
|
||||
return (
|
||||
<VStack smallgap>
|
||||
<div class="flex justify-between">
|
||||
<SmallHeader>Outbound</SmallHeader>
|
||||
<SmallHeader>Inbound</SmallHeader>
|
||||
</div>
|
||||
<div class="flex gap-1 w-full">
|
||||
<div
|
||||
class="bg-m-green p-2 rounded-l-xl min-w-fit"
|
||||
style={{
|
||||
"flex-grow": props.outbound || 1
|
||||
}}
|
||||
>
|
||||
<AmountSmall amountSats={props.outbound} />
|
||||
</div>
|
||||
<div
|
||||
class="bg-m-blue p-2 rounded-r-xl min-w-fit"
|
||||
style={{
|
||||
"flex-grow": props.inbound || 1
|
||||
}}
|
||||
>
|
||||
<AmountSmall amountSats={props.inbound} />
|
||||
</div>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
export function LiquidityMonitor() {
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const [channelInfo] = createResource(async () => {
|
||||
const channels = await state.mutiny_wallet?.list_channels();
|
||||
let inbound = 0n;
|
||||
|
||||
for (const channel of channels) {
|
||||
inbound =
|
||||
inbound +
|
||||
BigInt(channel.size) -
|
||||
BigInt(channel.balance + channel.reserve);
|
||||
}
|
||||
|
||||
return { inbound, channelCount: channels?.length };
|
||||
});
|
||||
|
||||
return (
|
||||
<Show when={channelInfo()?.channelCount}>
|
||||
<Card>
|
||||
<NiceP>
|
||||
You have {channelInfo()?.channelCount} lightning{" "}
|
||||
{channelInfo()?.channelCount === 1 ? "channel" : "channels"}
|
||||
.
|
||||
</NiceP>{" "}
|
||||
<BalanceBar
|
||||
inbound={Number(channelInfo()?.inbound) || 0}
|
||||
outbound={Number(state.balance?.lightning) || 0}
|
||||
/>
|
||||
<TinyText>
|
||||
Outbound is the amount of money you can spend on lightning.
|
||||
Inbound is the amount you can receive without incurring a
|
||||
lightning service fee.
|
||||
</TinyText>
|
||||
</Card>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Card, NiceP, VStack } from "~/components/layout";
|
||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { downloadTextFile } from "~/utils/download";
|
||||
|
||||
@@ -11,11 +11,13 @@ export function Logs() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<InnerCard title="Download debug logs">
|
||||
<VStack>
|
||||
<NiceP>Something screwy going on? Check out the logs!</NiceP>
|
||||
<Button onClick={handleSave}>Download Logs</Button>
|
||||
<Button intent="green" onClick={handleSave}>
|
||||
Download Logs
|
||||
</Button>
|
||||
</VStack>
|
||||
</Card>
|
||||
</InnerCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { QRCodeSVG } from "solid-qr-code";
|
||||
import { As, Dialog } from "@kobalte/core";
|
||||
import { Button, Card } from "~/components/layout";
|
||||
import { Button, Card, InnerCard, NiceP } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { createResource, Show } from "solid-js";
|
||||
|
||||
@@ -37,43 +37,47 @@ export default function NostrWalletConnectModal() {
|
||||
|
||||
// TODO: a lot of this markup is probably reusable as a "Modal" component
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<As component={Button}>Show Nostr Wallet Connect URI</As>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class={OVERLAY} />
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<Dialog.Title class={SMALL_HEADER}>
|
||||
Nostr Wallet Connect
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton class="dialog__close-button">
|
||||
<code>X</code>
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<Show when={connectionURI()}>
|
||||
<div class="w-full bg-white rounded-xl">
|
||||
<QRCodeSVG
|
||||
value={connectionURI() || ""}
|
||||
class="w-full h-full p-8 max-h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<code class="break-all">
|
||||
{connectionURI() || ""}
|
||||
</code>
|
||||
</Card>
|
||||
</Show>
|
||||
<Button onClick={toggleNwc}>
|
||||
{state.nwc_enabled ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<InnerCard title="Nostr Wallet Connect">
|
||||
<NiceP>Test out some nostr stuff.</NiceP>
|
||||
<div />
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<As component={Button}>Show Nostr Wallet Connect URI</As>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class={OVERLAY} />
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<Dialog.Title class={SMALL_HEADER}>
|
||||
Nostr Wallet Connect
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton class="dialog__close-button">
|
||||
<code>X</code>
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<Show when={connectionURI()}>
|
||||
<div class="w-full bg-white rounded-xl">
|
||||
<QRCodeSVG
|
||||
value={connectionURI() || ""}
|
||||
class="w-full h-full p-8 max-h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<code class="break-all">
|
||||
{connectionURI() || ""}
|
||||
</code>
|
||||
</Card>
|
||||
</Show>
|
||||
<Button onClick={toggleNwc}>
|
||||
{state.nwc_enabled ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</InnerCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { QRCodeSVG } from "solid-qr-code";
|
||||
import { As, Dialog } from "@kobalte/core";
|
||||
import { Button, Card } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Show, createResource } from "solid-js";
|
||||
import { getExistingSettings } from "~/logic/mutinyWalletSetup";
|
||||
import getHostname from "~/utils/getHostname";
|
||||
|
||||
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";
|
||||
const DIALOG_CONTENT =
|
||||
"w-[80vw] max-w-[400px] p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||
const SMALL_HEADER = "text-sm font-semibold uppercase";
|
||||
|
||||
export default function PeerConnectModal() {
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const getPeerConnectString = async () => {
|
||||
if (state.mutiny_wallet) {
|
||||
const { proxy } = getExistingSettings();
|
||||
const nodes = await state.mutiny_wallet.list_nodes();
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
const hostName = getHostname(proxy || "");
|
||||
const connectString = `mutiny:${firstNode}@${hostName}`;
|
||||
return connectString;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const [peerConnectString] = createResource(getPeerConnectString);
|
||||
|
||||
// TODO: a lot of this markup is probably reusable as a "Modal" component
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<As component={Button}>Show Peer Connect Info</As>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class={OVERLAY} />
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<Dialog.Title class={SMALL_HEADER}>
|
||||
Peer connect info
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton class="dialog__close-button">
|
||||
<code>X</code>
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<Show when={peerConnectString()}>
|
||||
<div class="w-full bg-white rounded-xl">
|
||||
<QRCodeSVG
|
||||
value={peerConnectString() || ""}
|
||||
class="w-full h-full p-8 max-h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<code class="break-all">
|
||||
{peerConnectString() || ""}
|
||||
</code>
|
||||
</Card>
|
||||
</Show>
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,34 @@
|
||||
import { Button, Card, NiceP, VStack } from "~/components/layout";
|
||||
import { createSignal } from "solid-js";
|
||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function Restart() {
|
||||
const [state, _] = useMegaStore();
|
||||
const [hasStopped, setHasStopped] = createSignal(false);
|
||||
|
||||
async function handleStop() {
|
||||
await state.mutiny_wallet?.stop();
|
||||
async function toggle() {
|
||||
if (hasStopped()) {
|
||||
await state.mutiny_wallet?.start();
|
||||
setHasStopped(false);
|
||||
} else {
|
||||
await state.mutiny_wallet?.stop();
|
||||
setHasStopped(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<InnerCard>
|
||||
<VStack>
|
||||
<NiceP>
|
||||
Something *extra* screwy going on? Stop the nodes!
|
||||
</NiceP>
|
||||
<Button onClick={handleStop}>Stop</Button>
|
||||
<Button
|
||||
intent={hasStopped() ? "green" : "red"}
|
||||
onClick={toggle}
|
||||
>
|
||||
{hasStopped() ? "Start" : "Stop"}
|
||||
</Button>
|
||||
</VStack>
|
||||
</Card>
|
||||
</InnerCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { For, Match, Switch, createMemo, createSignal } from "solid-js";
|
||||
import { useCopy } from "~/utils/useCopy";
|
||||
import copyIcon from "~/assets/icons/copy.svg";
|
||||
|
||||
export function SeedWords(props: {
|
||||
words: string;
|
||||
setHasSeen?: (hasSeen: boolean) => void;
|
||||
}) {
|
||||
const [shouldShow, setShouldShow] = createSignal(false);
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
function toggleShow() {
|
||||
setShouldShow(!shouldShow());
|
||||
@@ -15,28 +18,61 @@ export function SeedWords(props: {
|
||||
|
||||
const splitWords = createMemo(() => props.words.split(" "));
|
||||
|
||||
function dangerouslyCopy() {
|
||||
copy(props.words);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden"
|
||||
onClick={toggleShow}
|
||||
>
|
||||
<div class="flex flex-col gap-4 bg-m-red p-4 rounded-xl overflow-hidden">
|
||||
<Switch>
|
||||
<Match when={!shouldShow()}>
|
||||
<div class="cursor-pointer">
|
||||
<div
|
||||
class="cursor-pointer flex w-full justify-center"
|
||||
onClick={toggleShow}
|
||||
>
|
||||
<code class="text-red">TAP TO REVEAL SEED WORDS</code>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={shouldShow()}>
|
||||
<ol class="cursor-pointer overflow-hidden grid grid-cols-2 w-full list-decimal list-inside">
|
||||
<For each={splitWords()}>
|
||||
{(word) => (
|
||||
<li class="font-mono text-left">{word}</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
<>
|
||||
<div
|
||||
class="cursor-pointer flex w-full justify-center"
|
||||
onClick={toggleShow}
|
||||
>
|
||||
<code class="text-red">HIDE</code>
|
||||
</div>
|
||||
<ol class="overflow-hidden columns-2 w-full list-decimal list-inside">
|
||||
<For each={splitWords()}>
|
||||
{(word) => (
|
||||
<li class="font-mono text-left min-w-fit bg">
|
||||
{word}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
<div class="flex w-full justify-center">
|
||||
<button
|
||||
onClick={dangerouslyCopy}
|
||||
class="bg-white/10 hover:bg-white/20 p-2 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>
|
||||
{copied()
|
||||
? "Copied!"
|
||||
: "Dangerously Copy to Clipboard"}
|
||||
</span>
|
||||
<img
|
||||
src={copyIcon}
|
||||
alt="copy"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import {
|
||||
MutinyWalletSettingStrings,
|
||||
getExistingSettings
|
||||
} from "~/logic/mutinyWalletSetup";
|
||||
import { Button, Card, SmallHeader } from "~/components/layout";
|
||||
import { Button, Card, NiceP } from "~/components/layout";
|
||||
import { showToast } from "./Toaster";
|
||||
import eify from "~/utils/eify";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { ExternalLink } from "./layout/ExternalLink";
|
||||
|
||||
export function SettingsStringsEditor() {
|
||||
const existingSettings = getExistingSettings();
|
||||
const [_settingsForm, { Form, Field }] =
|
||||
const [settingsForm, { Form, Field }] =
|
||||
createForm<MutinyWalletSettingStrings>({
|
||||
initialValues: existingSettings
|
||||
});
|
||||
@@ -19,8 +20,7 @@ export function SettingsStringsEditor() {
|
||||
|
||||
async function handleSubmit(values: MutinyWalletSettingStrings) {
|
||||
try {
|
||||
const existing = getExistingSettings();
|
||||
const newSettings = { ...existing, ...values };
|
||||
const newSettings = { ...existingSettings, ...values };
|
||||
await actions.setupMutinyWallet(newSettings);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
@@ -31,16 +31,15 @@ export function SettingsStringsEditor() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card title="Servers">
|
||||
<Form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
||||
<h2 class="text-2xl font-light">
|
||||
<NiceP>
|
||||
Don't trust us! Use your own servers to back Mutiny.
|
||||
</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
<SmallHeader>Network</SmallHeader>
|
||||
<pre>{existingSettings.network}</pre>
|
||||
</div>
|
||||
|
||||
</NiceP>
|
||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Self-hosting">
|
||||
Learn more about self-hosting
|
||||
</ExternalLink>
|
||||
<div />
|
||||
<Field
|
||||
name="proxy"
|
||||
validate={[url("Should be a url starting with wss://")]}
|
||||
@@ -51,6 +50,7 @@ export function SettingsStringsEditor() {
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="Websockets Proxy"
|
||||
caption="How your lightning node communicates with the rest of the network."
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
@@ -64,6 +64,7 @@ export function SettingsStringsEditor() {
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="Esplora"
|
||||
caption="Block data for on-chain information."
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
@@ -77,6 +78,7 @@ export function SettingsStringsEditor() {
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="RGS"
|
||||
caption="Rapid Gossip Sync. Network data about the lightning network used for routing."
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
@@ -90,10 +92,18 @@ export function SettingsStringsEditor() {
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="LSP"
|
||||
caption="Lightning Service Provider. Automatically opens channels to you for inbound liquidity. Also wraps invoices for privacy."
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Button type="submit">Save</Button>
|
||||
<div />
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!settingsForm.dirty}
|
||||
intent="blue"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -34,9 +34,9 @@ export function ShareButton(props: { receiveString: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TruncateMiddle(props: { text: string }) {
|
||||
export function TruncateMiddle(props: { text: string }) {
|
||||
return (
|
||||
<div class="flex text-neutral-400 font-mono">
|
||||
<div class="flex text-neutral-300 font-mono">
|
||||
<span class="truncate">{props.text}</span>
|
||||
<span class="pr-2">
|
||||
{props.text.length > 8 ? props.text.slice(-8) : ""}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { TextField as KTextField } from "@kobalte/core";
|
||||
import { type JSX, Show, splitProps } from "solid-js";
|
||||
import { TinyText } from ".";
|
||||
|
||||
type TextFieldProps = {
|
||||
name: string;
|
||||
type?: "text" | "email" | "tel" | "password" | "url" | "date";
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
caption?: string;
|
||||
value: string | undefined;
|
||||
error: string;
|
||||
required?: boolean;
|
||||
@@ -36,7 +38,7 @@ export function TextField(props: TextFieldProps) {
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
validationState={props.error ? "invalid" : "valid"}
|
||||
isRequired={props.required}
|
||||
required={props.required}
|
||||
>
|
||||
<Show when={props.label}>
|
||||
<KTextField.Label class="text-sm uppercase font-semibold">
|
||||
@@ -60,6 +62,9 @@ export function TextField(props: TextFieldProps) {
|
||||
/>
|
||||
</Show>
|
||||
<KTextField.ErrorMessage>{props.error}</KTextField.ErrorMessage>
|
||||
<Show when={props.caption}>
|
||||
<TinyText>{props.caption}</TinyText>
|
||||
</Show>
|
||||
</KTextField.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,9 +146,19 @@ export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (
|
||||
);
|
||||
};
|
||||
|
||||
export const VStack: ParentComponent<{ biggap?: boolean }> = (props) => {
|
||||
export const VStack: ParentComponent<{
|
||||
biggap?: boolean;
|
||||
smallgap?: boolean;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div class={`flex flex-col gap-${props.biggap ? "8" : "4"}`}>
|
||||
<div
|
||||
class="flex flex-col"
|
||||
classList={{
|
||||
"gap-2": props.smallgap,
|
||||
"gap-8": props.biggap,
|
||||
"gap-4": !props.biggap && !props.smallgap
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
@@ -178,6 +188,10 @@ export const NiceP: ParentComponent = (props) => {
|
||||
return <p class="text-xl font-light">{props.children}</p>;
|
||||
};
|
||||
|
||||
export const TinyText: ParentComponent = (props) => {
|
||||
return <p class="text-neutral-400 text-sm">{props.children}</p>;
|
||||
};
|
||||
|
||||
export const TinyButton: ParentComponent<{
|
||||
onClick: () => void;
|
||||
tag?: MutinyTagItem;
|
||||
|
||||
@@ -2,10 +2,10 @@ import { DeleteEverything } from "~/components/DeleteEverything";
|
||||
import KitchenSink from "~/components/KitchenSink";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import {
|
||||
Card,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
MutinyWalletGuard,
|
||||
NiceP,
|
||||
SafeArea,
|
||||
SmallHeader,
|
||||
VStack
|
||||
@@ -18,14 +18,16 @@ export default function Admin() {
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<BackLink href="/settings" title="Settings" />
|
||||
<LargeHeader>Admin</LargeHeader>
|
||||
<LargeHeader>Secret Debug Tools</LargeHeader>
|
||||
<VStack>
|
||||
<Card>
|
||||
<p>
|
||||
If you know what you're doing you're in the
|
||||
right place!
|
||||
</p>
|
||||
</Card>
|
||||
<NiceP>
|
||||
If you know what you're doing you're in the right
|
||||
place.
|
||||
</NiceP>
|
||||
<NiceP>
|
||||
These are internal tools we use to debug and test
|
||||
the app. Please be careful!
|
||||
</NiceP>
|
||||
<KitchenSink />
|
||||
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden">
|
||||
<SmallHeader>Danger zone</SmallHeader>
|
||||
|
||||
@@ -505,7 +505,7 @@ export default function Send() {
|
||||
!destination() ||
|
||||
sending() ||
|
||||
amountSats() === 0n ||
|
||||
insufficientFunds() ||
|
||||
!!insufficientFunds() ||
|
||||
!!error()
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {
|
||||
ButtonLink,
|
||||
Card,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
MutinyWalletGuard,
|
||||
NiceP,
|
||||
SafeArea,
|
||||
VStack
|
||||
} from "~/components/layout";
|
||||
import { BackLink } from "~/components/layout/BackLink";
|
||||
import { Logs } from "~/components/Logs";
|
||||
import { Restart } from "~/components/Restart";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { SeedWords } from "~/components/SeedWords";
|
||||
import { SettingsStringsEditor } from "~/components/SettingsStringsEditor";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { LiquidityMonitor } from "~/components/LiquidityMonitor";
|
||||
|
||||
export default function Settings() {
|
||||
const [store, _actions] = useMegaStore();
|
||||
@@ -24,20 +25,35 @@ export default function Settings() {
|
||||
<BackLink />
|
||||
<LargeHeader>Settings</LargeHeader>
|
||||
<VStack biggap>
|
||||
<VStack>
|
||||
<p class="text-2xl font-light">
|
||||
Write down these words or you'll die!
|
||||
</p>
|
||||
<SeedWords
|
||||
words={store.mutiny_wallet?.show_seed() || ""}
|
||||
/>
|
||||
</VStack>
|
||||
<LiquidityMonitor />
|
||||
<Card title="Backup your seed words">
|
||||
<VStack>
|
||||
<NiceP>
|
||||
These 12 words allow you to recover your
|
||||
on-chain funds in case you lose your device
|
||||
or clear your browser storage.
|
||||
</NiceP>
|
||||
<SeedWords
|
||||
words={
|
||||
store.mutiny_wallet?.show_seed() || ""
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
</Card>
|
||||
<SettingsStringsEditor />
|
||||
<Logs />
|
||||
<Restart />
|
||||
<ButtonLink href="/admin">
|
||||
"I know what I'm doing"
|
||||
</ButtonLink>
|
||||
<Card title="If you know what you're doing">
|
||||
<VStack>
|
||||
<NiceP>
|
||||
We have some not-very-pretty debug tools we
|
||||
use to test the wallet. Use wisely!
|
||||
</NiceP>
|
||||
<div class="flex justify-center">
|
||||
<ButtonLink href="/admin" layout="xs">
|
||||
Secret Debug Tools
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</VStack>
|
||||
</Card>
|
||||
</VStack>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="settings" />
|
||||
|
||||
Reference in New Issue
Block a user