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