mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-01-07 16:24:29 +01:00
design touchup (#184)
This commit is contained in:
3
src/assets/icons/tinyArrow.svg
Normal file
3
src/assets/icons/tinyArrow.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 216 B |
@@ -174,7 +174,9 @@ export function CombinedActivity(props: { limit?: number }) {
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
<Match when={activity.state === "ready" && activity().length === 0}>
|
||||
<NiceP>No activity to show</NiceP>
|
||||
<div class="w-full text-center">
|
||||
<NiceP>Receive some sats get started</NiceP>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={activity.state === "ready" && activity().length >= 0}>
|
||||
<For each={activity.latest}>
|
||||
@@ -198,5 +200,5 @@ export function CombinedActivity(props: { limit?: number }) {
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,17 +10,17 @@ function prettyPrintAmount(n?: number | bigint): string {
|
||||
}
|
||||
|
||||
export function Amount(props: {
|
||||
amountSats: bigint | number | undefined
|
||||
showFiat?: boolean
|
||||
loading?: boolean
|
||||
amountSats: bigint | number | undefined;
|
||||
showFiat?: boolean;
|
||||
loading?: boolean;
|
||||
centered?: boolean;
|
||||
}) {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const amountInUsd = () =>
|
||||
satsToUsd(state.price, Number(props.amountSats) || 0, true)
|
||||
const amountInUsd = () => satsToUsd(state.price, Number(props.amountSats) || 0, true);
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2" classList={{ "items-center": props.centered }}>
|
||||
<h1 class="text-4xl font-light">
|
||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}
|
||||
<span class="text-xl">SATS</span>
|
||||
@@ -32,7 +32,7 @@ export function Amount(props: {
|
||||
</h2>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function AmountSmall(props: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CombinedActivity } from './Activity';
|
||||
import userClock from '~/assets/icons/user-clock.svg';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { Show } from 'solid-js';
|
||||
import { ExternalLink } from "./layout/ExternalLink";
|
||||
|
||||
export default function App() {
|
||||
const [state, _actions] = useMegaStore();
|
||||
@@ -44,13 +45,11 @@ export default function App() {
|
||||
</Card>
|
||||
<p class="self-center text-neutral-500 mt-4">
|
||||
Bugs? Feedback?{" "}
|
||||
<a
|
||||
class="text-neutral-400"
|
||||
target="_blank"
|
||||
href="https://github.com/MutinyWallet/mutiny-web/issues"
|
||||
>
|
||||
Create an issue
|
||||
</a>
|
||||
<span class="text-neutral-400">
|
||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
|
||||
Create an issue
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</p>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="home" />
|
||||
|
||||
@@ -1,73 +1,76 @@
|
||||
import { Show, Suspense } from "solid-js";
|
||||
import { Show } from "solid-js";
|
||||
import { Button, FancyCard, Indicator } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Amount } from "./Amount";
|
||||
import { A, useNavigate } from "solid-start";
|
||||
import shuffle from "~/assets/icons/shuffle.svg"
|
||||
|
||||
function prettyPrintAmount(n?: number | bigint): string {
|
||||
if (!n || n.valueOf() === 0) {
|
||||
return "0"
|
||||
}
|
||||
return n.toLocaleString()
|
||||
}
|
||||
import shuffle from "~/assets/icons/shuffle.svg";
|
||||
|
||||
export function LoadingShimmer() {
|
||||
return (<div class="flex flex-col gap-2 animate-pulse">
|
||||
<h1 class="text-4xl font-light">
|
||||
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]" />
|
||||
</h1>
|
||||
<h2 class="text-xl font-light text-white/70" >
|
||||
<div class="w-[8rem] rounded bg-neutral-700 h-[1.75rem]" />
|
||||
</h2>
|
||||
</div>)
|
||||
return (
|
||||
<div class="flex flex-col gap-2 animate-pulse">
|
||||
<h1 class="text-4xl font-light">
|
||||
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]" />
|
||||
</h1>
|
||||
<h2 class="text-xl font-light text-white/70">
|
||||
<div class="w-[8rem] rounded bg-neutral-700 h-[1.75rem]" />
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const STYLE = "px-2 py-1 rounded-xl border border-neutral-400 text-sm flex gap-2 items-center font-semibold"
|
||||
const STYLE =
|
||||
"px-2 py-1 rounded-xl border border-neutral-400 text-sm flex gap-2 items-center font-semibold";
|
||||
|
||||
export default function BalanceBox(props: { loading?: boolean }) {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const emptyBalance = () => (state.balance?.confirmed || 0n) === 0n && (state.balance?.lightning || 0n) === 0n
|
||||
const emptyBalance = () =>
|
||||
(state.balance?.confirmed || 0n) === 0n &&
|
||||
(state.balance?.lightning || 0n) === 0n &&
|
||||
(state.balance?.unconfirmed || 0n) === 0n;
|
||||
|
||||
const navigate = useNavigate()
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FancyCard title="Lightning">
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
||||
</Show>
|
||||
</FancyCard>
|
||||
const totalOnchain = () => (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
|
||||
|
||||
<FancyCard title="On-Chain" tag={state.is_syncing && <Indicator>Syncing</Indicator>}>
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<div class="flex justify-between">
|
||||
<Amount amountSats={state.balance?.confirmed} showFiat />
|
||||
<div class="self-end justify-self-end">
|
||||
<A href="/swap" class={STYLE}>
|
||||
<img src={shuffle} alt="swap" class="h-8 w-8" />
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Suspense>
|
||||
<Show when={state.balance?.unconfirmed}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<header class='text-sm font-semibold uppercase text-white/50'>
|
||||
Unconfirmed Balance
|
||||
</header>
|
||||
<div class="text-white/50">
|
||||
{prettyPrintAmount(state.balance?.unconfirmed)} <span class='text-sm'>SATS</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Suspense>
|
||||
</FancyCard>
|
||||
<div class="flex gap-2 py-4">
|
||||
<Button onClick={() => navigate("/send")} disabled={emptyBalance() || props.loading} intent="green">Send</Button>
|
||||
<Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">Receive</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<FancyCard title="Lightning">
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
||||
</Show>
|
||||
</FancyCard>
|
||||
|
||||
<FancyCard
|
||||
title="On-Chain"
|
||||
subtitle={state.balance?.unconfirmed ? "Unconfirmed" : undefined}
|
||||
tag={state.is_syncing && <Indicator>Syncing</Indicator>}
|
||||
>
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<div class="flex justify-between">
|
||||
<Amount amountSats={totalOnchain()} showFiat />
|
||||
<Show when={!emptyBalance()}>
|
||||
<div class="self-end justify-self-end">
|
||||
<A href="/swap" class={STYLE}>
|
||||
<img src={shuffle} alt="swap" class="h-8 w-8" />
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</FancyCard>
|
||||
<div class="flex gap-2 py-4">
|
||||
<Button
|
||||
onClick={() => navigate("/send")}
|
||||
disabled={emptyBalance() || props.loading}
|
||||
intent="green"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/receive")} disabled={props.loading} intent="blue">
|
||||
Receive
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,170 +11,187 @@ import { ConfirmDialog } from "~/components/Dialog";
|
||||
import { showToast } from "~/components/Toaster";
|
||||
import { ImportExport } from "~/components/ImportExport";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { ExternalLink } from "./layout/ExternalLink";
|
||||
|
||||
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
|
||||
type RefetchPeersType = (info?: unknown) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined
|
||||
type RefetchPeersType = (
|
||||
info?: unknown
|
||||
) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined;
|
||||
|
||||
function PeerItem(props: { peer: MutinyPeer }) {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const handleDisconnectPeer = async () => {
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = nodes[0] as string || ""
|
||||
const handleDisconnectPeer = async () => {
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
|
||||
if (props.peer.is_connected) {
|
||||
await state.mutiny_wallet?.disconnect_peer(firstNode, props.peer.pubkey);
|
||||
} else {
|
||||
await state.mutiny_wallet?.delete_peer(firstNode, props.peer.pubkey);
|
||||
}
|
||||
};
|
||||
if (props.peer.is_connected) {
|
||||
await state.mutiny_wallet?.disconnect_peer(firstNode, props.peer.pubkey);
|
||||
} else {
|
||||
await state.mutiny_wallet?.delete_peer(firstNode, props.peer.pubkey);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||
{">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey}
|
||||
</h2>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<VStack>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(props.peer, null, 2)}
|
||||
</pre>
|
||||
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>Disconnect</Button>
|
||||
</VStack>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
)
|
||||
return (
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||
{">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey}
|
||||
</h2>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<VStack>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(props.peer, null, 2)}
|
||||
</pre>
|
||||
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</VStack>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersList() {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const getPeers = async () => {
|
||||
return await state.mutiny_wallet?.list_peers() as Promise<MutinyPeer[]>
|
||||
};
|
||||
const getPeers = async () => {
|
||||
return (await state.mutiny_wallet?.list_peers()) as Promise<MutinyPeer[]>;
|
||||
};
|
||||
|
||||
const [peers, { refetch }] = createResource(getPeers);
|
||||
const [peers, { refetch }] = createResource(getPeers);
|
||||
|
||||
createEffect(() => {
|
||||
// refetch peers every 5 seconds
|
||||
const interval = setTimeout(() => {
|
||||
refetch();
|
||||
}, 5000);
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
})
|
||||
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>
|
||||
<ConnectPeer refetchPeers={refetch} />
|
||||
</>
|
||||
)
|
||||
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>
|
||||
<ConnectPeer refetchPeers={refetch} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const [value, setValue] = createSignal("");
|
||||
const [value, setValue] = createSignal("");
|
||||
|
||||
const onSubmit = async (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
const onSubmit = async (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const peerConnectString = value().trim();
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = nodes[0] as string || ""
|
||||
const peerConnectString = value().trim();
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
|
||||
await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString)
|
||||
await state.mutiny_wallet?.connect_to_peer(firstNode, peerConnectString);
|
||||
|
||||
await props.refetchPeers()
|
||||
await props.refetchPeers();
|
||||
|
||||
setValue("");
|
||||
};
|
||||
setValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<InnerCard>
|
||||
<form class="flex flex-col gap-4" onSubmit={onSubmit} >
|
||||
<TextField.Root
|
||||
value={value()}
|
||||
onChange={setValue}
|
||||
validationState={(value() == "") ? "valid" : "invalid"}
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase" >Connect Peer</TextField.Label>
|
||||
<TextField.Input class="w-full p-2 rounded-lg text-black" placeholder="mutiny:028241..." />
|
||||
<TextField.ErrorMessage class="text-red-500">Expecting something like mutiny:abc123...</TextField.ErrorMessage>
|
||||
</TextField.Root>
|
||||
<Button layout="small" type="submit">Connect</Button>
|
||||
</form >
|
||||
</InnerCard>
|
||||
)
|
||||
return (
|
||||
<InnerCard>
|
||||
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||
<TextField.Root
|
||||
value={value()}
|
||||
onChange={setValue}
|
||||
validationState={value() == "" ? "valid" : "invalid"}
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase">Connect Peer</TextField.Label>
|
||||
<TextField.Input
|
||||
class="w-full p-2 rounded-lg text-black"
|
||||
placeholder="mutiny:028241..."
|
||||
/>
|
||||
<TextField.ErrorMessage class="text-red-500">
|
||||
Expecting something like mutiny:abc123...
|
||||
</TextField.ErrorMessage>
|
||||
</TextField.Root>
|
||||
<Button layout="small" type="submit">
|
||||
Connect
|
||||
</Button>
|
||||
</form>
|
||||
</InnerCard>
|
||||
);
|
||||
}
|
||||
|
||||
type RefetchChannelsListType = (
|
||||
info?: unknown
|
||||
) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | null | undefined;
|
||||
|
||||
type RefetchChannelsListType = (info?: unknown) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | null | undefined
|
||||
function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
function ChannelItem(props: { channel: MutinyChannel, network?: Network }) {
|
||||
const [state, _] = useMegaStore()
|
||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
||||
|
||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
||||
function handleCloseChannel() {
|
||||
setConfirmOpen(true);
|
||||
}
|
||||
|
||||
function handleCloseChannel() {
|
||||
setConfirmOpen(true);
|
||||
async function confirmCloseChannel() {
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
await state.mutiny_wallet?.close_channel(props.channel.outpoint as string);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
}
|
||||
setConfirmLoading(false);
|
||||
setConfirmOpen(false);
|
||||
}
|
||||
|
||||
async function confirmCloseChannel() {
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
await state.mutiny_wallet?.close_channel(props.channel.outpoint as string)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
}
|
||||
setConfirmLoading(false);
|
||||
setConfirmOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||
{">"} {props.channel.peer}
|
||||
</h2>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<VStack>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(props.channel, null, 2)}
|
||||
</pre>
|
||||
<a class="" href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)} target="_blank" rel="noreferrer">
|
||||
Mempool Link
|
||||
</a>
|
||||
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>Close Channel</Button>
|
||||
|
||||
</VStack>
|
||||
<ConfirmDialog open={confirmOpen()} onConfirm={confirmCloseChannel} onCancel={() => setConfirmOpen(false)} loading={confirmLoading()}>
|
||||
<p>Are you sure you want to close this channel?</p>
|
||||
</ConfirmDialog>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
)
|
||||
return (
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||
{">"} {props.channel.peer}
|
||||
</h2>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<VStack>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(props.channel, null, 2)}
|
||||
</pre>
|
||||
<ExternalLink href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)}>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>
|
||||
Close Channel
|
||||
</Button>
|
||||
</VStack>
|
||||
<ConfirmDialog
|
||||
open={confirmOpen()}
|
||||
onConfirm={confirmCloseChannel}
|
||||
onCancel={() => setConfirmOpen(false)}
|
||||
loading={confirmLoading()}
|
||||
>
|
||||
<p>Are you sure you want to close this channel?</p>
|
||||
</ConfirmDialog>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelsList() {
|
||||
@@ -258,44 +275,40 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InnerCard>
|
||||
<form class="flex flex-col gap-4" onSubmit={onSubmit} >
|
||||
<TextField.Root
|
||||
value={peerPubkey()}
|
||||
onChange={setPeerPubkey}
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase" >Pubkey</TextField.Label>
|
||||
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
||||
</TextField.Root>
|
||||
<TextField.Root
|
||||
value={amount()}
|
||||
onChange={setAmount}
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase" >Amount</TextField.Label>
|
||||
<TextField.Input
|
||||
type="number"
|
||||
class="w-full p-2 rounded-lg text-black" />
|
||||
</TextField.Root>
|
||||
<Button layout="small" type="submit">Open Channel</Button>
|
||||
</form >
|
||||
</InnerCard>
|
||||
<Show when={newChannel()}>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
||||
</pre>
|
||||
<pre>{newChannel()?.outpoint}</pre>
|
||||
<a class="text-sm font-light opacity-50 mt-2" href={mempoolTxUrl(newChannel()?.outpoint?.split(":")[0], network)} target="_blank" rel="noreferrer">
|
||||
Mempool Link
|
||||
</a>
|
||||
</Show>
|
||||
<Show when={creationError()}>
|
||||
<pre>{creationError()?.message}</pre>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
<>
|
||||
<InnerCard>
|
||||
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||
<TextField.Root
|
||||
value={peerPubkey()}
|
||||
onChange={setPeerPubkey}
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase">Pubkey</TextField.Label>
|
||||
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
||||
</TextField.Root>
|
||||
<TextField.Root value={amount()} onChange={setAmount} class="flex flex-col gap-2">
|
||||
<TextField.Label class="text-sm font-semibold uppercase">Amount</TextField.Label>
|
||||
<TextField.Input type="number" class="w-full p-2 rounded-lg text-black" />
|
||||
</TextField.Root>
|
||||
<Button layout="small" type="submit">
|
||||
Open Channel
|
||||
</Button>
|
||||
</form>
|
||||
</InnerCard>
|
||||
<Show when={newChannel()}>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(newChannel()?.outpoint, null, 2)}
|
||||
</pre>
|
||||
<pre>{newChannel()?.outpoint}</pre>
|
||||
<ExternalLink href={mempoolTxUrl(newChannel()?.outpoint?.split(":")[0], network)}>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
</Show>
|
||||
<Show when={creationError()}>
|
||||
<pre>{creationError()?.message}</pre>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LnUrlAuth() {
|
||||
|
||||
@@ -4,29 +4,33 @@ import { Dynamic } from "solid-js/web";
|
||||
import { A } from "solid-start";
|
||||
import { LoadingSpinner } from ".";
|
||||
|
||||
const button = cva("p-3 rounded-xl font-semibold disabled:opacity-50 disabled:grayscale transition", {
|
||||
const button = cva(
|
||||
"p-3 rounded-xl font-semibold disabled:opacity-20 disabled:grayscale transition",
|
||||
{
|
||||
variants: {
|
||||
// TODO: button hover has to work different than buttonlinks (like disabled state)
|
||||
intent: {
|
||||
active: "bg-white text-black border border-white hover:text-[#3B6CCC]",
|
||||
inactive: "bg-black text-white border border-white hover:text-[#3B6CCC]",
|
||||
glowy: "bg-black/10 shadow-xl text-white border border-m-blue hover:m-blue-dark hover:text-m-blue",
|
||||
blue: "bg-m-blue text-white shadow-inner-button hover:bg-m-blue-dark text-shadow-button",
|
||||
red: "bg-m-red text-white shadow-inner-button hover:bg-m-red-dark text-shadow-button",
|
||||
green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button",
|
||||
},
|
||||
layout: {
|
||||
flex: "flex-1 text-xl",
|
||||
pad: "px-8 text-xl",
|
||||
small: "px-4 py-2 w-auto text-lg",
|
||||
xs: "px-4 py-2 w-auto rounded-lg text-base"
|
||||
},
|
||||
// TODO: button hover has to work different than buttonlinks (like disabled state)
|
||||
intent: {
|
||||
active: "bg-white text-black border border-white hover:text-[#3B6CCC]",
|
||||
inactive: "bg-black text-white border border-white hover:text-[#3B6CCC]",
|
||||
glowy:
|
||||
"bg-black/10 shadow-xl text-white border border-m-blue hover:m-blue-dark hover:text-m-blue",
|
||||
blue: "bg-m-blue text-white shadow-inner-button hover:bg-m-blue-dark text-shadow-button",
|
||||
red: "bg-m-red text-white shadow-inner-button hover:bg-m-red-dark text-shadow-button",
|
||||
green: "bg-m-green text-white shadow-inner-button hover:bg-m-green-dark text-shadow-button"
|
||||
},
|
||||
layout: {
|
||||
flex: "flex-1 text-xl",
|
||||
pad: "px-8 text-xl",
|
||||
small: "px-4 py-2 w-auto text-lg",
|
||||
xs: "px-4 py-2 w-auto rounded-lg text-base"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
intent: "inactive",
|
||||
layout: "flex"
|
||||
},
|
||||
});
|
||||
intent: "inactive",
|
||||
layout: "flex"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Help from https://github.com/arpadgabor/credee/blob/main/packages/www/src/components/ui/button.tsx
|
||||
|
||||
|
||||
21
src/components/layout/ExternalLink.tsx
Normal file
21
src/components/layout/ExternalLink.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ParentComponent } from "solid-js";
|
||||
|
||||
export const ExternalLink: ParentComponent<{ href: string }> = (props) => {
|
||||
return (
|
||||
<a target="_blank" rel="noopener noreferrer" href={props.href}>
|
||||
{props.children}{" "}
|
||||
<svg
|
||||
class="inline-block"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { JSX } from "solid-js";
|
||||
import { Button, LargeHeader } from "~/components/layout";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||
|
||||
type FullscreenModalProps = {
|
||||
title: string,
|
||||
open: boolean,
|
||||
setOpen: (open: boolean) => void,
|
||||
children?: JSX.Element,
|
||||
onConfirm?: () => void
|
||||
confirmText?: string
|
||||
}
|
||||
|
||||
export function FullscreenModal(props: FullscreenModalProps) {
|
||||
|
||||
const onNice = () => {
|
||||
props.onConfirm ? props.onConfirm() : props.setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||
<Dialog.Portal>
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<Dialog.Title>
|
||||
<LargeHeader>
|
||||
{props.title}
|
||||
</LargeHeader>
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton class="p-2 hover:bg-white/10 rounded-lg active:bg-m-blue">
|
||||
<img src={close} alt="Close" />
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
{props.children}
|
||||
</Dialog.Description>
|
||||
<div class="w-full flex">
|
||||
<Button onClick={onNice}>{props.confirmText ?? "Nice"}</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root >
|
||||
)
|
||||
}
|
||||
@@ -6,34 +6,49 @@ type Choices = { value: string, label: string, caption: string }[]
|
||||
// TODO: how could would it be if we could just pass the estimated fees in here?
|
||||
export function StyledRadioGroup(props: { value: string, choices: Choices, onValueChange: (value: string) => void, small?: boolean, accent?: "red" | "white" }) {
|
||||
return (
|
||||
// TODO: rewrite this with CVA, props are bad for tailwind
|
||||
<RadioGroup.Root value={props.value} onChange={props.onValueChange}
|
||||
class={"grid w-full gap-4"}
|
||||
classList={{ "grid-cols-2": props.choices.length === 2, "grid-cols-3": props.choices.length === 3, "gap-2": props.small }}
|
||||
>
|
||||
<For each={props.choices}>
|
||||
{choice =>
|
||||
<RadioGroup.Item value={choice.value}
|
||||
class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2`}
|
||||
classList={{ "ui-checked:outline-m-red": props.accent === "red", "ui-checked:outline-white": props.accent === "white", "ui-checked:outline-black/50 ui-checked:bg-white/10": props.choices.length === 1 }}
|
||||
// TODO: rewrite this with CVA, props are bad for tailwind
|
||||
<RadioGroup.Root
|
||||
value={props.value}
|
||||
onChange={props.onValueChange}
|
||||
class={"grid w-full gap-4"}
|
||||
classList={{
|
||||
"grid-cols-2": props.choices.length === 2,
|
||||
"grid-cols-3": props.choices.length === 3,
|
||||
"gap-2": props.small
|
||||
}}
|
||||
>
|
||||
<For each={props.choices}>
|
||||
{(choice) => (
|
||||
<RadioGroup.Item
|
||||
value={choice.value}
|
||||
class={`ui-checked:bg-neutral-950 bg-white/10 rounded outline outline-black/50 ui-checked:outline-m-blue ui-checked:outline-2`}
|
||||
classList={{
|
||||
"ui-checked:outline-m-red": props.accent === "red",
|
||||
"ui-checked:outline-white": props.accent === "white"
|
||||
}}
|
||||
>
|
||||
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
|
||||
<RadioGroup.ItemInput />
|
||||
<RadioGroup.ItemControl>
|
||||
<RadioGroup.ItemIndicator />
|
||||
</RadioGroup.ItemControl>
|
||||
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
||||
<div class="block">
|
||||
<div
|
||||
classList={{ "text-base": props.small, "text-lg": !props.small }}
|
||||
class={`font-semibold max-sm:text-sm`}
|
||||
>
|
||||
<div class={props.small ? "py-2 px-2" : "py-3 px-4"}>
|
||||
<RadioGroup.ItemInput />
|
||||
<RadioGroup.ItemControl >
|
||||
<RadioGroup.ItemIndicator />
|
||||
</RadioGroup.ItemControl>
|
||||
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
||||
<div class="block">
|
||||
<div classList={{ "text-base": props.small, "text-lg": !props.small }} class={`font-semibold max-sm:text-sm`}>{choice.label}</div>
|
||||
<Show when={!props.small}>
|
||||
<div class="text-sm font-light">{choice.caption}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</RadioGroup.ItemLabel>
|
||||
</div>
|
||||
</RadioGroup.Item>
|
||||
}
|
||||
</For>
|
||||
</RadioGroup.Root>
|
||||
)
|
||||
{choice.label}
|
||||
</div>
|
||||
<Show when={!props.small}>
|
||||
<div class="text-sm font-light">{choice.caption}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</RadioGroup.ItemLabel>
|
||||
</div>
|
||||
</RadioGroup.Item>
|
||||
)}
|
||||
</For>
|
||||
</RadioGroup.Root>
|
||||
);
|
||||
}
|
||||
@@ -37,17 +37,24 @@ export const InnerCard: ParentComponent<{ title?: string }> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const FancyCard: ParentComponent<{ title?: string, tag?: JSX.Element }> = (props) => {
|
||||
return (
|
||||
<div class='border border-black/50 rounded-xl border-b-4 p-4 flex flex-col gap-2 bg-neutral-800/50 shadow-fancy-card'>
|
||||
<div class="w-full flex justify-between items-center">
|
||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||
{props.tag && props.tag}
|
||||
</div>
|
||||
{props.children}
|
||||
export const FancyCard: ParentComponent<{
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
tag?: JSX.Element;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div class="border border-black/50 rounded-xl border-b-4 p-4 flex flex-col gap-2 bg-neutral-800/50 shadow-fancy-card">
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class="flex gap-2">
|
||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||
{props.subtitle && <SmallHeader class="text-neutral-500">{props.subtitle}</SmallHeader>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{props.tag && props.tag}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SafeArea: ParentComponent = (props) => {
|
||||
return (
|
||||
|
||||
5
src/components/successfail/MegaCheck.tsx
Normal file
5
src/components/successfail/MegaCheck.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import megacheck from "~/assets/icons/megacheck.png";
|
||||
|
||||
export function MegaCheck() {
|
||||
return <img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] flex-shrink" />;
|
||||
}
|
||||
5
src/components/successfail/MegaEx.tsx
Normal file
5
src/components/successfail/MegaEx.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import megaex from "~/assets/icons/megaex.png";
|
||||
|
||||
export function MegaEx() {
|
||||
return <img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />;
|
||||
}
|
||||
43
src/components/successfail/SuccessModal.tsx
Normal file
43
src/components/successfail/SuccessModal.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { JSX } from "solid-js";
|
||||
import { Button, LargeHeader } from "~/components/layout";
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||
|
||||
type SuccessModalProps = {
|
||||
title: string;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
children?: JSX.Element;
|
||||
onConfirm?: () => void;
|
||||
confirmText?: string;
|
||||
};
|
||||
|
||||
export function SuccessModal(props: SuccessModalProps) {
|
||||
const onNice = () => {
|
||||
props.onConfirm ? props.onConfirm() : props.setOpen(false);
|
||||
};
|
||||
|
||||
// <div class="flex flex-col items-center gap-8 h-full max-w-[400px]">
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||
<Dialog.Portal>
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<Dialog.Title>
|
||||
<LargeHeader>{props.title}</LargeHeader>
|
||||
</Dialog.Title>
|
||||
<div />
|
||||
</div>
|
||||
<Dialog.Description class="flex flex-col items-center justify-center gap-8 pb-4 h-full w-full max-w-[400px] mx-auto">
|
||||
{props.children}
|
||||
</Dialog.Description>
|
||||
<div class="w-full flex max-w-[300px] mx-auto">
|
||||
<Button onClick={onNice}>{props.confirmText ?? "Nice"}</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -50,4 +50,4 @@ select {
|
||||
background-position: right 0.75rem center;
|
||||
background-size: 20px 20px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
@@ -7,254 +7,285 @@ import { useMegaStore } from "~/state/megaStore";
|
||||
import { objectToSearchParams } from "~/utils/objectToSearchParams";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
import { Amount } from "~/components/Amount";
|
||||
import { FullscreenModal } from "~/components/layout/FullscreenModal";
|
||||
import { BackLink } from "~/components/layout/BackLink";
|
||||
import { TagEditor } from "~/components/TagEditor";
|
||||
import { StyledRadioGroup } from "~/components/layout/Radio";
|
||||
import { showToast } from "~/components/Toaster";
|
||||
import { useNavigate } from "solid-start";
|
||||
import megacheck from "~/assets/icons/megacheck.png";
|
||||
import { AmountCard } from "~/components/AmountCard";
|
||||
import { ShareCard } from "~/components/ShareCard";
|
||||
import { BackButton } from "~/components/layout/BackButton";
|
||||
import { MutinyTagItem } from "~/utils/tags";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { SuccessModal } from "~/components/successfail/SuccessModal";
|
||||
import { MegaCheck } from "~/components/successfail/MegaCheck";
|
||||
import { ExternalLink } from "~/components/layout/ExternalLink";
|
||||
|
||||
type OnChainTx = {
|
||||
transaction: {
|
||||
version: number
|
||||
lock_time: number
|
||||
input: Array<{
|
||||
previous_output: string
|
||||
script_sig: string
|
||||
sequence: number
|
||||
witness: Array<string>
|
||||
}>
|
||||
output: Array<{
|
||||
value: number
|
||||
script_pubkey: string
|
||||
}>
|
||||
}
|
||||
txid: string
|
||||
received: number
|
||||
sent: number
|
||||
confirmation_time: {
|
||||
height: number
|
||||
timestamp: number
|
||||
}
|
||||
}
|
||||
transaction: {
|
||||
version: number;
|
||||
lock_time: number;
|
||||
input: Array<{
|
||||
previous_output: string;
|
||||
script_sig: string;
|
||||
sequence: number;
|
||||
witness: Array<string>;
|
||||
}>;
|
||||
output: Array<{
|
||||
value: number;
|
||||
script_pubkey: string;
|
||||
}>;
|
||||
};
|
||||
txid: string;
|
||||
received: number;
|
||||
sent: number;
|
||||
confirmation_time: {
|
||||
height: number;
|
||||
timestamp: number;
|
||||
};
|
||||
};
|
||||
|
||||
const RECEIVE_FLAVORS = [{ value: "unified", label: "Unified", caption: "Sender decides" }, { value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }]
|
||||
const RECEIVE_FLAVORS = [
|
||||
{ value: "unified", label: "Unified", caption: "Sender decides" },
|
||||
{ value: "lightning", label: "Lightning", caption: "Fast and cool" },
|
||||
{ value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }
|
||||
];
|
||||
|
||||
type ReceiveFlavor = "unified" | "lightning" | "onchain"
|
||||
type ReceiveState = "edit" | "show" | "paid"
|
||||
type ReceiveFlavor = "unified" | "lightning" | "onchain";
|
||||
type ReceiveState = "edit" | "show" | "paid";
|
||||
type PaidState = "lightning_paid" | "onchain_paid";
|
||||
|
||||
export default function Receive() {
|
||||
const [state, _actions] = useMegaStore()
|
||||
const navigate = useNavigate();
|
||||
const [state, _actions] = useMegaStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [amount, setAmount] = createSignal("")
|
||||
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit")
|
||||
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
||||
const [unified, setUnified] = createSignal("")
|
||||
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true)
|
||||
const [amount, setAmount] = createSignal("");
|
||||
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit");
|
||||
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
||||
const [unified, setUnified] = createSignal("");
|
||||
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true);
|
||||
|
||||
// Tagging stuff
|
||||
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
|
||||
// Tagging stuff
|
||||
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
|
||||
|
||||
// The data we get after a payment
|
||||
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
||||
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
|
||||
// The data we get after a payment
|
||||
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
||||
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
|
||||
|
||||
// The flavor of the receive
|
||||
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("unified");
|
||||
// The flavor of the receive
|
||||
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("unified");
|
||||
|
||||
const receiveString = createMemo(() => {
|
||||
if (unified() && receiveState() === "show") {
|
||||
if (flavor() === "unified") {
|
||||
return unified();
|
||||
} else if (flavor() === "lightning") {
|
||||
return bip21Raw()?.invoice ?? "";
|
||||
} else if (flavor() === "onchain") {
|
||||
return bip21Raw()?.address ?? "";
|
||||
}
|
||||
const receiveString = createMemo(() => {
|
||||
if (unified() && receiveState() === "show") {
|
||||
if (flavor() === "unified") {
|
||||
return unified();
|
||||
} else if (flavor() === "lightning") {
|
||||
return bip21Raw()?.invoice ?? "";
|
||||
} else if (flavor() === "onchain") {
|
||||
return bip21Raw()?.address ?? "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function clearAll() {
|
||||
setAmount("");
|
||||
setReceiveState("edit");
|
||||
setBip21Raw(undefined);
|
||||
setUnified("");
|
||||
setPaymentTx(undefined);
|
||||
setPaymentInvoice(undefined);
|
||||
setSelectedValues([]);
|
||||
}
|
||||
|
||||
async function processContacts(contacts: Partial<MutinyTagItem>[]): Promise<string[]> {
|
||||
console.log("Processing contacts", contacts);
|
||||
|
||||
if (contacts.length) {
|
||||
const first = contacts![0];
|
||||
|
||||
if (!first.name) {
|
||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!first.id && first.name) {
|
||||
console.error("Creating new contact", first.name);
|
||||
const c = new Contact(first.name, undefined, undefined, undefined);
|
||||
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
|
||||
if (newContactId) {
|
||||
return [newContactId];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
setAmount("")
|
||||
setReceiveState("edit")
|
||||
setBip21Raw(undefined)
|
||||
setUnified("")
|
||||
setPaymentTx(undefined)
|
||||
setPaymentInvoice(undefined)
|
||||
setSelectedValues([])
|
||||
if (first.id) {
|
||||
console.error("Using existing contact", first.name, first.id);
|
||||
return [first.id];
|
||||
}
|
||||
}
|
||||
|
||||
async function processContacts(contacts: Partial<MutinyTagItem>[]): Promise<string[]> {
|
||||
console.log("Processing contacts", contacts)
|
||||
console.error("Something went wrong with contact creation, proceeding anyway");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (contacts.length) {
|
||||
const first = contacts![0];
|
||||
async function getUnifiedQr(amount: string) {
|
||||
const bigAmount = BigInt(amount);
|
||||
try {
|
||||
const tags = await processContacts(selectedValues());
|
||||
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tags);
|
||||
// Save the raw info so we can watch the address and invoice
|
||||
setBip21Raw(raw);
|
||||
|
||||
if (!first.name) {
|
||||
console.error("Something went wrong with contact creation, proceeding anyway")
|
||||
return []
|
||||
}
|
||||
|
||||
if (!first.id && first.name) {
|
||||
console.error("Creating new contact", first.name)
|
||||
const c = new Contact(first.name, undefined, undefined, undefined);
|
||||
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
|
||||
if (newContactId) {
|
||||
return [newContactId];
|
||||
}
|
||||
}
|
||||
|
||||
if (first.id) {
|
||||
console.error("Using existing contact", first.name, first.id)
|
||||
return [first.id];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
console.error("Something went wrong with contact creation, proceeding anyway")
|
||||
return []
|
||||
const params = objectToSearchParams({
|
||||
amount: raw?.btc_amount,
|
||||
lightning: raw?.invoice
|
||||
});
|
||||
|
||||
return `bitcoin:${raw?.address}?${params}`;
|
||||
} catch (e) {
|
||||
showToast(new Error("Couldn't create invoice. Are you asking for enough?"));
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function getUnifiedQr(amount: string) {
|
||||
const bigAmount = BigInt(amount);
|
||||
try {
|
||||
const tags = await processContacts(selectedValues());
|
||||
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tags);
|
||||
// Save the raw info so we can watch the address and invoice
|
||||
setBip21Raw(raw);
|
||||
async function onSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const params = objectToSearchParams({
|
||||
amount: raw?.btc_amount,
|
||||
lightning: raw?.invoice
|
||||
})
|
||||
const unifiedQr = await getUnifiedQr(amount());
|
||||
|
||||
return `bitcoin:${raw?.address}?${params}`
|
||||
setUnified(unifiedQr || "");
|
||||
setReceiveState("show");
|
||||
setShouldShowAmountEditor(false);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
showToast(new Error("Couldn't create invoice. Are you asking for enough?"))
|
||||
console.error(e)
|
||||
}
|
||||
async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise<PaidState | undefined> {
|
||||
if (bip21) {
|
||||
console.log("checking if paid...");
|
||||
const lightning = bip21.invoice;
|
||||
const address = bip21.address;
|
||||
|
||||
const invoice = await state.mutiny_wallet?.get_invoice(lightning);
|
||||
|
||||
if (invoice && invoice.paid) {
|
||||
setReceiveState("paid");
|
||||
setPaymentInvoice(invoice);
|
||||
return "lightning_paid";
|
||||
}
|
||||
|
||||
const tx = (await state.mutiny_wallet?.check_address(address)) as OnChainTx | undefined;
|
||||
|
||||
if (tx) {
|
||||
setReceiveState("paid");
|
||||
setPaymentTx(tx);
|
||||
return "onchain_paid";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid);
|
||||
|
||||
const unifiedQr = await getUnifiedQr(amount())
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
setUnified(unifiedQr || "")
|
||||
setReceiveState("show")
|
||||
setShouldShowAmountEditor(false)
|
||||
}
|
||||
|
||||
async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise<PaidState | undefined> {
|
||||
if (bip21) {
|
||||
console.log("checking if paid...")
|
||||
const lightning = bip21.invoice
|
||||
const address = bip21.address
|
||||
|
||||
const invoice = await state.mutiny_wallet?.get_invoice(lightning)
|
||||
|
||||
if (invoice && invoice.paid) {
|
||||
setReceiveState("paid")
|
||||
setPaymentInvoice(invoice)
|
||||
return "lightning_paid"
|
||||
}
|
||||
|
||||
const tx = await state.mutiny_wallet?.check_address(address) as OnChainTx | undefined;
|
||||
|
||||
if (tx) {
|
||||
setReceiveState("paid")
|
||||
setPaymentTx(tx)
|
||||
return "onchain_paid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid);
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
createEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (receiveState() === "show") refetch();
|
||||
}, 1000); // Poll every second
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
createEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (receiveState() === "show") refetch();
|
||||
}, 1000); // Poll every second
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<Show when={receiveState() === "show"} fallback={<BackLink />}>
|
||||
<BackButton onClick={() => setReceiveState("edit")} title="Edit" />
|
||||
</Show>
|
||||
<LargeHeader action={receiveState() === "show" && <Indicator>Checking</Indicator>}>Receive Bitcoin</LargeHeader>
|
||||
<Switch>
|
||||
<Match when={!unified() || receiveState() === "edit"}>
|
||||
<div class="flex flex-col flex-1 gap-8">
|
||||
<AmountCard initialOpen={shouldShowAmountEditor()} amountSats={amount() || "0"} setAmountSats={setAmount} isAmountEditable />
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<Show when={receiveState() === "show"} fallback={<BackLink />}>
|
||||
<BackButton onClick={() => setReceiveState("edit")} title="Edit" />
|
||||
</Show>
|
||||
<LargeHeader action={receiveState() === "show" && <Indicator>Checking</Indicator>}>
|
||||
Receive Bitcoin
|
||||
</LargeHeader>
|
||||
<Switch>
|
||||
<Match when={!unified() || receiveState() === "edit"}>
|
||||
<div class="flex flex-col flex-1 gap-8">
|
||||
<AmountCard
|
||||
initialOpen={shouldShowAmountEditor()}
|
||||
amountSats={amount() || "0"}
|
||||
setAmountSats={setAmount}
|
||||
isAmountEditable
|
||||
/>
|
||||
|
||||
<Card title="Private tags">
|
||||
<TagEditor selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Add the sender for your records" />
|
||||
</Card>
|
||||
<Card title="Private tags">
|
||||
<TagEditor
|
||||
selectedValues={selectedValues()}
|
||||
setSelectedValues={setSelectedValues}
|
||||
placeholder="Add the sender for your records"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div class="flex-1" />
|
||||
<Button class="w-full flex-grow-0" disabled={!amount()} intent="green" onClick={onSubmit}>Create Request</Button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={unified() && receiveState() === "show"}>
|
||||
<StyledRadioGroup small value={flavor()} onValueChange={setFlavor} choices={RECEIVE_FLAVORS} accent="white" />
|
||||
<div class="w-full bg-white rounded-xl">
|
||||
<QRCodeSVG value={receiveString() ?? ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||
</div>
|
||||
<p class="text-neutral-400 text-center">Show or share this code with the sender</p>
|
||||
<ShareCard text={receiveString() ?? ""} />
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
||||
<FullscreenModal
|
||||
title="Payment Received"
|
||||
open={!!paidState()}
|
||||
setOpen={(open: boolean) => { if (!open) clearAll() }}
|
||||
onConfirm={() => { clearAll(); navigate("/"); }}
|
||||
>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat />
|
||||
</div>
|
||||
</FullscreenModal>
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
|
||||
<FullscreenModal
|
||||
title="Payment Received"
|
||||
open={!!paidState()}
|
||||
setOpen={(open: boolean) => { if (!open) clearAll() }}
|
||||
onConfirm={() => { clearAll(); navigate("/"); }}
|
||||
>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<Amount amountSats={paymentTx()?.received} showFiat />
|
||||
<a href={mempoolTxUrl(paymentTx()?.txid, network)} target="_blank" rel="noreferrer">
|
||||
Mempool Link
|
||||
</a>
|
||||
</div>
|
||||
</FullscreenModal>
|
||||
</Match>
|
||||
</Switch>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="receive" />
|
||||
</SafeArea >
|
||||
</MutinyWalletGuard>
|
||||
)
|
||||
<div class="flex-1" />
|
||||
<Button
|
||||
class="w-full flex-grow-0"
|
||||
disabled={!amount()}
|
||||
intent="green"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={unified() && receiveState() === "show"}>
|
||||
<StyledRadioGroup
|
||||
small
|
||||
value={flavor()}
|
||||
onValueChange={setFlavor}
|
||||
choices={RECEIVE_FLAVORS}
|
||||
accent="white"
|
||||
/>
|
||||
<div class="w-full bg-white rounded-xl">
|
||||
<QRCodeSVG value={receiveString() ?? ""} class="w-full h-full p-8 max-h-[400px]" />
|
||||
</div>
|
||||
<p class="text-neutral-400 text-center">Show or share this code with the sender</p>
|
||||
<ShareCard text={receiveString() ?? ""} />
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
||||
<SuccessModal
|
||||
title="Payment Received"
|
||||
open={!!paidState()}
|
||||
setOpen={(open: boolean) => {
|
||||
if (!open) clearAll();
|
||||
}}
|
||||
onConfirm={() => {
|
||||
clearAll();
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
<MegaCheck />
|
||||
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat centered />
|
||||
</SuccessModal>
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
|
||||
<SuccessModal
|
||||
title="Payment Received"
|
||||
open={!!paidState()}
|
||||
setOpen={(open: boolean) => {
|
||||
if (!open) clearAll();
|
||||
}}
|
||||
onConfirm={() => {
|
||||
clearAll();
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
<MegaCheck />
|
||||
<Amount amountSats={paymentTx()?.received} showFiat centered />
|
||||
<ExternalLink href={mempoolTxUrl(paymentTx()?.txid, network)}>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
</SuccessModal>
|
||||
</Match>
|
||||
</Switch>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="receive" />
|
||||
</SafeArea>
|
||||
</MutinyWalletGuard>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { StyledRadioGroup } from "~/components/layout/Radio";
|
||||
import { ParsedParams, toParsedParams } from "./Scanner";
|
||||
import { showToast } from "~/components/Toaster";
|
||||
import eify from "~/utils/eify";
|
||||
import { FullscreenModal } from "~/components/layout/FullscreenModal";
|
||||
import megacheck from "~/assets/icons/megacheck.png";
|
||||
import megaex from "~/assets/icons/megaex.png";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
@@ -33,6 +32,8 @@ import { AmountCard } from "~/components/AmountCard";
|
||||
import { MutinyTagItem } from "~/utils/tags";
|
||||
import { BackButton } from "~/components/layout/BackButton";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { SuccessModal } from "~/components/successfail/SuccessModal";
|
||||
import { ExternalLink } from "~/components/layout/ExternalLink";
|
||||
|
||||
export type SendSource = "lightning" | "onchain";
|
||||
|
||||
@@ -121,7 +122,7 @@ function DestinationInput(props: {
|
||||
class="p-2 rounded-lg bg-white/10 placeholder-neutral-400"
|
||||
/>
|
||||
<Button disabled={!props.fieldDestination} intent="blue" onClick={props.handleDecode}>
|
||||
Decode
|
||||
Continue
|
||||
</Button>
|
||||
<HStack>
|
||||
<Button onClick={props.handlePaste}>
|
||||
@@ -408,8 +409,8 @@ export default function Send() {
|
||||
const sendButtonDisabled = createMemo(() => {
|
||||
return !destination() || sending() || amountSats() === 0n;
|
||||
});
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
@@ -419,7 +420,7 @@ export default function Send() {
|
||||
<BackButton onClick={() => clearAll()} title="Start Over" />
|
||||
</Show>
|
||||
<LargeHeader>Send Bitcoin</LargeHeader>
|
||||
<FullscreenModal
|
||||
<SuccessModal
|
||||
title={sentDetails()?.amount ? "Sent" : "Payment Failed"}
|
||||
confirmText={sentDetails()?.amount ? "Nice" : "Too Bad"}
|
||||
open={!!sentDetails()}
|
||||
@@ -431,30 +432,24 @@ export default function Send() {
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col items-center gap-8 h-full">
|
||||
<Switch>
|
||||
<Match when={sentDetails()?.failure_reason}>
|
||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||
{sentDetails()?.failure_reason}
|
||||
</p>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<Amount amountSats={sentDetails()?.amount} showFiat />
|
||||
<Show when={sentDetails()?.txid}>
|
||||
<a
|
||||
href={mempoolTxUrl(sentDetails()?.txid, network)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Mempool Link
|
||||
</a>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</FullscreenModal>
|
||||
<Switch>
|
||||
<Match when={sentDetails()?.failure_reason}>
|
||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||
{sentDetails()?.failure_reason}
|
||||
</p>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<Amount amountSats={sentDetails()?.amount} showFiat centered />
|
||||
<Show when={sentDetails()?.txid}>
|
||||
<ExternalLink href={mempoolTxUrl(sentDetails()?.txid, network)}>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</SuccessModal>
|
||||
<VStack biggap>
|
||||
<Switch>
|
||||
<Match when={address() || invoice() || nodePubkey() || lnurlp()}>
|
||||
@@ -463,7 +458,7 @@ export default function Send() {
|
||||
setSource={setSource}
|
||||
both={!!address() && !!invoice()}
|
||||
/>
|
||||
<Card>
|
||||
<Card title="Destination">
|
||||
<VStack>
|
||||
<DestinationShower
|
||||
source={source()}
|
||||
|
||||
@@ -7,12 +7,11 @@ import { showToast } from "~/components/Toaster";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
MutinyWalletGuard,
|
||||
SafeArea,
|
||||
VStack,
|
||||
VStack
|
||||
} from "~/components/layout";
|
||||
import { BackLink } from "~/components/layout/BackLink";
|
||||
import { TextField } from "~/components/layout/TextField";
|
||||
@@ -22,9 +21,11 @@ import eify from "~/utils/eify";
|
||||
import megaex from "~/assets/icons/megaex.png";
|
||||
import megacheck from "~/assets/icons/megacheck.png";
|
||||
import { InfoBox } from "~/components/InfoBox";
|
||||
import { FullscreenModal } from "~/components/layout/FullscreenModal";
|
||||
import { useNavigate } from "solid-start";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
import { SuccessModal } from "~/components/successfail/SuccessModal";
|
||||
import { ExternalLink } from "~/components/layout/ExternalLink";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
|
||||
const CHANNEL_FEE_ESTIMATE_ADDRESS =
|
||||
"bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf";
|
||||
@@ -44,7 +45,6 @@ export default function Swap() {
|
||||
|
||||
const [source, setSource] = createSignal<SendSource>("onchain");
|
||||
const [amountSats, setAmountSats] = createSignal(0n);
|
||||
const [useLsp, setUseLsp] = createSignal(true);
|
||||
const [isConnecting, setIsConnecting] = createSignal(false);
|
||||
|
||||
const [selectedPeer, setSelectedPeer] = createSignal<string>("");
|
||||
@@ -121,7 +121,7 @@ export default function Swap() {
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = (nodes[0] as string) || "";
|
||||
|
||||
if (useLsp()) {
|
||||
if (hasLsp()) {
|
||||
const new_channel = await state.mutiny_wallet?.open_channel(
|
||||
firstNode,
|
||||
undefined,
|
||||
@@ -147,7 +147,7 @@ export default function Swap() {
|
||||
|
||||
const canSwap = () => {
|
||||
const balance = (state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
|
||||
return (!!selectedPeer() || !!useLsp()) && amountSats() >= 10000n && amountSats() <= balance;
|
||||
return (!!selectedPeer() || !!hasLsp()) && amountSats() >= 10000n && amountSats() <= balance;
|
||||
};
|
||||
|
||||
const amountWarning = () => {
|
||||
@@ -165,14 +165,16 @@ export default function Swap() {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<BackLink />
|
||||
<LargeHeader>Swap to Lightning</LargeHeader>
|
||||
<FullscreenModal
|
||||
title={channelOpenResult()?.channel ? "Channel Opened" : "Channel Open Failed"}
|
||||
<SuccessModal
|
||||
title={channelOpenResult()?.channel ? "Swap Success" : "Swap Failed"}
|
||||
confirmText={channelOpenResult()?.channel ? "Nice" : "Too Bad"}
|
||||
open={!!channelOpenResult()}
|
||||
setOpen={(open: boolean) => {
|
||||
@@ -183,50 +185,38 @@ export default function Swap() {
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col items-center gap-8 pb-8">
|
||||
<Switch>
|
||||
<Match when={channelOpenResult()?.failure_reason}>
|
||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
||||
<Switch>
|
||||
<Match when={channelOpenResult()?.failure_reason}>
|
||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
||||
|
||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||
{channelOpenResult()?.failure_reason?.message}
|
||||
</p>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img
|
||||
src={megacheck}
|
||||
alt="success"
|
||||
class="w-1/2 mx-auto max-w-[30vh] flex-shrink"
|
||||
/>
|
||||
<AmountCard
|
||||
amountSats={channelOpenResult()?.channel?.balance?.toString() || ""}
|
||||
reserve={channelOpenResult()?.channel?.reserve?.toString() || ""}
|
||||
/>
|
||||
<Show when={channelOpenResult()?.channel?.outpoint}>
|
||||
<a
|
||||
class=""
|
||||
href={mempoolTxUrl(
|
||||
channelOpenResult()?.channel?.outpoint?.split(":")[0],
|
||||
"signet"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Mempool Link
|
||||
</a>
|
||||
</Show>
|
||||
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</FullscreenModal>
|
||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">
|
||||
{channelOpenResult()?.failure_reason?.message}
|
||||
</p>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[30vh] flex-shrink" />
|
||||
<AmountCard
|
||||
amountSats={channelOpenResult()?.channel?.balance?.toString() || ""}
|
||||
reserve={channelOpenResult()?.channel?.reserve?.toString() || ""}
|
||||
/>
|
||||
<Show when={channelOpenResult()?.channel?.outpoint}>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
channelOpenResult()?.channel?.outpoint?.split(":")[0],
|
||||
network
|
||||
)}
|
||||
>
|
||||
View Transaction
|
||||
</ExternalLink>
|
||||
</Show>
|
||||
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
|
||||
</Match>
|
||||
</Switch>
|
||||
</SuccessModal>
|
||||
<VStack biggap>
|
||||
<MethodChooser source={source()} setSource={setSource} both={false} />
|
||||
<VStack>
|
||||
<Show when={hasLsp()}>
|
||||
<Checkbox checked={useLsp()} onChange={setUseLsp} label="Use LSP" />
|
||||
</Show>
|
||||
<Show when={!useLsp()}>
|
||||
<Show when={!hasLsp()}>
|
||||
<Card>
|
||||
<VStack>
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
|
||||
Reference in New Issue
Block a user