clean up settings and debug screens

This commit is contained in:
Paul Miller
2023-06-07 14:31:46 -05:00
parent 03f5ab667e
commit 3eba44f8c7
16 changed files with 369 additions and 276 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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 />
</>
); );
} }

View 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>
);
}

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>
);
}

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>
); );

View File

@@ -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) : ""}

View File

@@ -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>
); );
} }

View File

@@ -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;

View File

@@ -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>

View File

@@ -505,7 +505,7 @@ export default function Send() {
!destination() || !destination() ||
sending() || sending() ||
amountSats() === 0n || amountSats() === 0n ||
insufficientFunds() || !!insufficientFunds() ||
!!error() !!error()
); );
}); });

View File

@@ -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" />