mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-01-22 15:44:19 +01:00
feat: redesign payment id screen
This commit is contained in:
@@ -11,8 +11,8 @@ import {
|
||||
import { A } from "solid-start";
|
||||
|
||||
import {
|
||||
ActivityDetailsModal,
|
||||
ActivityItem,
|
||||
DetailsIdModal,
|
||||
HackActivityType,
|
||||
LoadingShimmer,
|
||||
NiceP
|
||||
@@ -30,7 +30,7 @@ export const REDSHIFT_LABEL =
|
||||
"py-1 px-2 bg-white text-m-red rounded inline-block text-sm";
|
||||
export const RIGHT_COLUMN = "flex flex-col items-right text-right max-w-[8rem]";
|
||||
|
||||
interface IActivityItem {
|
||||
export interface IActivityItem {
|
||||
kind: HackActivityType;
|
||||
id: string;
|
||||
amount_sats: number;
|
||||
@@ -50,7 +50,6 @@ function UnifiedActivityItem(props: {
|
||||
props.item.kind as unknown as HackActivityType
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ActivityItem
|
||||
// This is actually the ActivityType enum but wasm is hard
|
||||
@@ -77,7 +76,6 @@ export function CombinedActivity(props: { limit?: number }) {
|
||||
function openDetailsModal(id: string, kind: HackActivityType) {
|
||||
console.log("Opening details modal: ", id, kind);
|
||||
|
||||
// Some old channels don't have a channel id in the activity list
|
||||
if (!id) {
|
||||
console.warn("No id provided to openDetailsModal");
|
||||
return;
|
||||
@@ -109,7 +107,7 @@ export function CombinedActivity(props: { limit?: number }) {
|
||||
fallback={<LoadingShimmer />}
|
||||
>
|
||||
<Show when={detailsId() && detailsKind()}>
|
||||
<DetailsIdModal
|
||||
<ActivityDetailsModal
|
||||
open={detailsOpen()}
|
||||
kind={detailsKind()}
|
||||
id={detailsId()}
|
||||
|
||||
597
src/components/ActivityDetailsModal.tsx
Normal file
597
src/components/ActivityDetailsModal.tsx
Normal file
@@ -0,0 +1,597 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import {
|
||||
MutinyChannel,
|
||||
MutinyInvoice,
|
||||
TagItem
|
||||
} from "@mutinywallet/mutiny-wasm";
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
Match,
|
||||
Show,
|
||||
Suspense,
|
||||
Switch
|
||||
} from "solid-js";
|
||||
|
||||
import bolt from "~/assets/icons/bolt.svg";
|
||||
import chain from "~/assets/icons/chain.svg";
|
||||
import copyIcon from "~/assets/icons/copy.svg";
|
||||
import shuffle from "~/assets/icons/shuffle.svg";
|
||||
import {
|
||||
ActivityAmount,
|
||||
AmountFiat,
|
||||
AmountSats,
|
||||
FancyCard,
|
||||
HackActivityType,
|
||||
Hr,
|
||||
InfoBox,
|
||||
KeyValue,
|
||||
ModalCloseButton,
|
||||
TinyButton,
|
||||
TruncateMiddle,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { BalanceBar } from "~/routes/settings/Channels";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { mempoolTxUrl, prettyPrintTime, useCopy } from "~/utils";
|
||||
|
||||
interface ChannelClosure {
|
||||
channel_id: string;
|
||||
node_id: string;
|
||||
reason: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface OnChainTx {
|
||||
txid: string;
|
||||
received: number;
|
||||
sent: number;
|
||||
fee?: number;
|
||||
confirmation_time?: {
|
||||
Confirmed?: {
|
||||
height: number;
|
||||
time: number;
|
||||
};
|
||||
};
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||
export const DIALOG_POSITIONER =
|
||||
"fixed inset-0 z-50 flex items-center justify-center";
|
||||
export const DIALOG_CONTENT =
|
||||
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||
|
||||
function LightningHeader(props: { info: MutinyInvoice }) {
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="flex flex-row items-center justify-center gap-[4px] font-normal">
|
||||
{props.info.inbound
|
||||
? i18n.t("activity.transaction_details.lightning_receive")
|
||||
: i18n.t("activity.transaction_details.lightning_send")}
|
||||
<img src={bolt} alt="lightning bolt" class="h-4 w-4" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="text-2xl"
|
||||
classList={{ "text-m-green": props.info.inbound }}
|
||||
>
|
||||
<AmountSats
|
||||
amountSats={props.info.amount_sats}
|
||||
icon={props.info.inbound ? "plus" : undefined}
|
||||
denominationSize="lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-lg text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={props.info.amount_sats}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
const i18n = useI18n();
|
||||
|
||||
const isSend = () => {
|
||||
return props.info.sent > props.info.received;
|
||||
};
|
||||
|
||||
const amount = () => {
|
||||
if (isSend()) {
|
||||
return (props.info.sent - props.info.received).toString();
|
||||
} else {
|
||||
return (props.info.received - props.info.sent).toString();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="flex flex-row items-center justify-center gap-[4px] font-normal">
|
||||
{props.kind === "ChannelOpen"
|
||||
? i18n.t("activity.transaction_details.channel_open")
|
||||
: props.kind === "ChannelClose"
|
||||
? i18n.t("activity.transaction_details.channel_close")
|
||||
: isSend()
|
||||
? i18n.t("activity.transaction_details.onchain_send")
|
||||
: i18n.t("activity.transaction_details.onchain_receive")}
|
||||
<Switch>
|
||||
<Match
|
||||
when={
|
||||
props.kind === "ChannelOpen" ||
|
||||
props.kind === "ChannelClose"
|
||||
}
|
||||
>
|
||||
<img src={shuffle} alt="swap" class="h-4 w-4" />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={chain} alt="blockchain" class="h-4 w-4" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<Show when={props.kind !== "ChannelClose"}>
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="text-2xl"
|
||||
classList={{ "text-m-green": !isSend() }}
|
||||
>
|
||||
<AmountSats
|
||||
amountSats={Number(amount())}
|
||||
icon={!isSend() ? "plus" : undefined}
|
||||
denominationSize="lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-lg text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={Number(amount())}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MiniStringShower(props: { text: string }) {
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
return (
|
||||
<div class="grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1">
|
||||
<TruncateMiddle text={props.text} />
|
||||
<button
|
||||
class="w-[1.5rem] p-1"
|
||||
classList={{ "bg-m-green rounded": copied() }}
|
||||
onClick={() => copy(props.text)}
|
||||
>
|
||||
<img src={copyIcon} alt="copy" class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormatPrettyPrint(props: { ts: number }) {
|
||||
return (
|
||||
<div>
|
||||
{prettyPrintTime(props.ts).split(",", 2).join(",")}
|
||||
<div class="text-right text-sm text-white/70">
|
||||
{prettyPrintTime(props.ts).split(", ")[2]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LightningDetails(props: { info: MutinyInvoice; tags?: TagItem }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
return (
|
||||
<VStack>
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key={i18n.t("activity.transaction_details.fee")}>
|
||||
<ActivityAmount
|
||||
amount={props.info.fees_paid!.toString()}
|
||||
price={state.price}
|
||||
/>
|
||||
</KeyValue>
|
||||
<Show when={props.tags || props.info.labels[0]}>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.tagged_to")}
|
||||
>
|
||||
<TinyButton
|
||||
tag={props.tags?.value ?? undefined}
|
||||
onClick={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
{props.tags?.name || props.info.labels[0]}
|
||||
</TinyButton>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.status")}>
|
||||
{props.info.paid
|
||||
? i18n.t("activity.transaction_details.paid")
|
||||
: i18n.t("activity.transaction_details.unpaid")}
|
||||
</KeyValue>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.date")}>
|
||||
<FormatPrettyPrint ts={Number(props.info.last_updated)} />
|
||||
</KeyValue>
|
||||
<Show when={props.info.description}>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.description")}
|
||||
>
|
||||
<span class="pl-6">{props.info.description}</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.invoice")}>
|
||||
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.payment_hash")}
|
||||
>
|
||||
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue
|
||||
key={i18n.t(
|
||||
"activity.transaction_details.payment_preimage"
|
||||
)}
|
||||
>
|
||||
<MiniStringShower text={props.info.preimage ?? ""} />
|
||||
</KeyValue>
|
||||
</ul>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function OnchainDetails(props: {
|
||||
info: OnChainTx;
|
||||
kind?: HackActivityType;
|
||||
tags?: TagItem;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
const confirmationTime = () => {
|
||||
return props.info.confirmation_time?.Confirmed?.time;
|
||||
};
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
// Can return nothing if the channel is already closed
|
||||
const [channelInfo] = createResource(async () => {
|
||||
if (props.kind === "ChannelOpen") {
|
||||
try {
|
||||
const channels =
|
||||
await (state.mutiny_wallet?.list_channels() as Promise<
|
||||
MutinyChannel[]
|
||||
>);
|
||||
const channel = channels.find(
|
||||
(channel) => channel.outpoint?.startsWith(props.info.txid)
|
||||
);
|
||||
return channel;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
|
||||
<ul class="flex flex-col gap-4">
|
||||
<Switch>
|
||||
<Match when={props.kind === "ChannelOpen" && channelInfo()}>
|
||||
<BalanceBar
|
||||
inbound={
|
||||
Number(channelInfo()?.size) -
|
||||
(Number(channelInfo()?.balance) +
|
||||
Number(channelInfo()?.reserve)) || 0
|
||||
}
|
||||
reserve={Number(channelInfo()?.reserve) || 0}
|
||||
outbound={Number(channelInfo()?.balance) || 0}
|
||||
/>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.total")}
|
||||
>
|
||||
<ActivityAmount
|
||||
amount={channelInfo()!.size.toString()}
|
||||
price={state.price}
|
||||
/>
|
||||
</KeyValue>
|
||||
<KeyValue
|
||||
key={i18n.t(
|
||||
"activity.transaction_details.onchain_fee"
|
||||
)}
|
||||
>
|
||||
<ActivityAmount
|
||||
amount={props.info.fee!.toString()}
|
||||
price={state.price}
|
||||
/>
|
||||
</KeyValue>
|
||||
</Match>
|
||||
<Match when={props.kind === "ChannelOpen"}>
|
||||
<InfoBox accent="blue">
|
||||
{i18n.t("activity.transaction_details.no_details")}
|
||||
</InfoBox>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show
|
||||
when={
|
||||
props.kind !== "ChannelOpen" &&
|
||||
props.info.fee &&
|
||||
props.info.fee > 0
|
||||
}
|
||||
>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.onchain_fee")}
|
||||
>
|
||||
<ActivityAmount
|
||||
amount={props.info.fee!.toString()}
|
||||
price={state.price}
|
||||
/>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<Show when={props.tags && props.kind === "OnChain"}>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.tagged_to")}
|
||||
>
|
||||
<TinyButton
|
||||
tag={props.tags?.value ?? undefined}
|
||||
onClick={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
{props.tags?.name || props.info.labels[0]}
|
||||
</TinyButton>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.status")}>
|
||||
{confirmationTime()
|
||||
? i18n.t("activity.transaction_details.confirmed")
|
||||
: i18n.t("activity.transaction_details.unconfirmed")}
|
||||
</KeyValue>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.date")}>
|
||||
{confirmationTime() ? (
|
||||
<FormatPrettyPrint ts={Number(confirmationTime())} />
|
||||
) : (
|
||||
"Pending"
|
||||
)}
|
||||
</KeyValue>
|
||||
<Show when={props.kind === "ChannelOpen" && channelInfo()}>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.peer")}>
|
||||
<MiniStringShower text={channelInfo()?.peer ?? ""} />
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.txid")}>
|
||||
<div class="flex gap-1">
|
||||
{/* Have to do all these shenanigans because css / html is hard */}
|
||||
<div class="grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={mempoolTxUrl(props.info.txid, network)}
|
||||
>
|
||||
<div class="flex flex-nowrap items-center font-mono text-white">
|
||||
<span class="truncate">
|
||||
{props.info.txid}
|
||||
</span>
|
||||
<span class="-ml-5">
|
||||
|
||||
{props.info.txid.length > 32
|
||||
? props.info.txid.slice(-8)
|
||||
: ""}
|
||||
</span>
|
||||
<svg
|
||||
class="inline-block w-[16px] overflow-visible pl-0.5 text-white"
|
||||
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>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
class="min-w-[1.5rem] p-1"
|
||||
classList={{ "bg-m-green rounded": copied() }}
|
||||
onClick={() => copy(props.info.txid)}
|
||||
>
|
||||
<img src={copyIcon} alt="copy" class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</KeyValue>
|
||||
</ul>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelCloseDetails(props: { info: ChannelClosure }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<VStack>
|
||||
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
|
||||
<ul class="flex flex-col gap-4">
|
||||
<InfoBox accent="blue">
|
||||
<p>{i18n.t("activity.transaction_details.sweep_delay")}</p>
|
||||
</InfoBox>
|
||||
<KeyValue
|
||||
key={i18n.t("activity.transaction_details.channel_id")}
|
||||
>
|
||||
<MiniStringShower text={props.info.channel_id ?? ""} />
|
||||
</KeyValue>
|
||||
<Show when={props.info.timestamp}>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.date")}>
|
||||
{props.info.timestamp ? (
|
||||
<FormatPrettyPrint
|
||||
ts={Number(props.info.timestamp)}
|
||||
/>
|
||||
) : (
|
||||
i18n.t("common.pending")
|
||||
)}
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("activity.transaction_details.reason")}>
|
||||
<p class="whitespace-normal text-right text-neutral-300">
|
||||
{props.info.reason ?? ""}
|
||||
</p>
|
||||
</KeyValue>
|
||||
</ul>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActivityDetailsModal(props: {
|
||||
open: boolean;
|
||||
kind?: HackActivityType;
|
||||
id: string;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const id = () => props.id;
|
||||
const kind = () => props.kind;
|
||||
|
||||
const [data, { refetch }] = createResource(async () => {
|
||||
try {
|
||||
if (kind() === "Lightning") {
|
||||
console.debug("reading invoice: ", id());
|
||||
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(
|
||||
id()
|
||||
);
|
||||
return invoice;
|
||||
} else if (kind() === "ChannelClose") {
|
||||
console.debug("reading channel close: ", id());
|
||||
const closeItem =
|
||||
await state.mutiny_wallet?.get_channel_closure(id());
|
||||
|
||||
return closeItem;
|
||||
} else {
|
||||
console.debug("reading tx: ", id());
|
||||
const tx = await state.mutiny_wallet?.get_transaction(id());
|
||||
|
||||
return tx;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
const tags = createMemo(() => {
|
||||
if (
|
||||
!!data() &&
|
||||
data()?.labels !== undefined &&
|
||||
typeof data()?.labels[0] === "string"
|
||||
) {
|
||||
try {
|
||||
// find if there's just one for now
|
||||
const contacts = state.mutiny_wallet?.get_contact(
|
||||
data().labels[0]
|
||||
);
|
||||
if (contacts) {
|
||||
return contacts;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (props.id && props.kind && props.open) {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class={OVERLAY} />
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<Suspense>
|
||||
<div class="p-4">
|
||||
<div class="flex justify-between">
|
||||
<div />
|
||||
<Dialog.CloseButton>
|
||||
<ModalCloseButton />
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Title>
|
||||
<FancyCard>
|
||||
<Switch>
|
||||
<Match
|
||||
when={kind() === "Lightning"}
|
||||
>
|
||||
<LightningHeader
|
||||
info={
|
||||
data() as MutinyInvoice
|
||||
}
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
kind() === "OnChain" ||
|
||||
kind() === "ChannelOpen" ||
|
||||
kind() === "ChannelClose"
|
||||
}
|
||||
>
|
||||
<OnchainHeader
|
||||
info={data() as OnChainTx}
|
||||
kind={kind()}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</FancyCard>
|
||||
</Dialog.Title>
|
||||
<Hr />
|
||||
<Switch>
|
||||
<Match when={kind() === "Lightning"}>
|
||||
<LightningDetails
|
||||
info={data() as MutinyInvoice}
|
||||
tags={tags()}
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
kind() === "OnChain" ||
|
||||
kind() === "ChannelOpen"
|
||||
}
|
||||
>
|
||||
<OnchainDetails
|
||||
info={data() as OnChainTx}
|
||||
kind={kind()}
|
||||
tags={tags()}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={kind() === "ChannelClose"}>
|
||||
<ChannelCloseDetails
|
||||
info={data() as ChannelClosure}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Suspense>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -102,9 +102,9 @@ export function AmountSmall(props: {
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<span class="font-light">
|
||||
<span class="text-sm font-light md:text-base">
|
||||
{prettyPrintAmount(props.amountSats)}
|
||||
<span class="text-sm">
|
||||
<span class="text-xs md:text-sm">
|
||||
{props.amountSats === 1 || props.amountSats === 1n
|
||||
? i18n.t("common.sat")
|
||||
: i18n.t("common.sats")}
|
||||
|
||||
@@ -9,7 +9,9 @@ const noop = () => {
|
||||
// do nothing
|
||||
};
|
||||
|
||||
const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => {
|
||||
const AmountKeyValue: ParentComponent<{ key: string; gray?: boolean }> = (
|
||||
props
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
class="flex items-center justify-between"
|
||||
@@ -55,12 +57,12 @@ function USDShower(props: { amountSats: string; fee?: string }) {
|
||||
|
||||
return (
|
||||
<Show when={!(props.amountSats === "0")}>
|
||||
<KeyValue gray key="">
|
||||
<AmountKeyValue gray key="">
|
||||
<div class="self-end">
|
||||
{amountInFiat()}
|
||||
<span class="text-sm">{state.fiat.value}</span>
|
||||
</div>
|
||||
</KeyValue>
|
||||
</AmountKeyValue>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
@@ -99,7 +101,7 @@ export function AmountCard(props: {
|
||||
<Switch>
|
||||
<Match when={props.fee}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key={i18n.t("receive.amount")}>
|
||||
<AmountKeyValue key={i18n.t("receive.amount")}>
|
||||
<Show
|
||||
when={props.isAmountEditable}
|
||||
fallback={
|
||||
@@ -124,16 +126,16 @@ export function AmountCard(props: {
|
||||
fee={props.fee}
|
||||
/>
|
||||
</Show>
|
||||
</KeyValue>
|
||||
<KeyValue gray key={i18n.t("receive.fee")}>
|
||||
</AmountKeyValue>
|
||||
<AmountKeyValue gray key={i18n.t("receive.fee")}>
|
||||
<InlineAmount amount={props.fee || "0"} />
|
||||
</KeyValue>
|
||||
</AmountKeyValue>
|
||||
</div>
|
||||
<hr class="border-white/20" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key={i18n.t("receive.total")}>
|
||||
<AmountKeyValue key={i18n.t("receive.total")}>
|
||||
<InlineAmount amount={totalOrTotalLessFee()} />
|
||||
</KeyValue>
|
||||
</AmountKeyValue>
|
||||
<USDShower
|
||||
amountSats={props.amountSats}
|
||||
fee={props.fee}
|
||||
@@ -142,26 +144,28 @@ export function AmountCard(props: {
|
||||
</Match>
|
||||
<Match when={props.reserve}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key={i18n.t("receive.channel_size")}>
|
||||
<AmountKeyValue
|
||||
key={i18n.t("receive.channel_size")}
|
||||
>
|
||||
<InlineAmount
|
||||
amount={add(
|
||||
props.amountSats,
|
||||
props.reserve
|
||||
).toString()}
|
||||
/>
|
||||
</KeyValue>
|
||||
<KeyValue
|
||||
</AmountKeyValue>
|
||||
<AmountKeyValue
|
||||
gray
|
||||
key={i18n.t("receive.channel_reserve")}
|
||||
>
|
||||
<InlineAmount amount={props.reserve || "0"} />
|
||||
</KeyValue>
|
||||
</AmountKeyValue>
|
||||
</div>
|
||||
<hr class="border-white/20" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key={i18n.t("receive.spendable")}>
|
||||
<AmountKeyValue key={i18n.t("receive.spendable")}>
|
||||
<InlineAmount amount={props.amountSats} />
|
||||
</KeyValue>
|
||||
</AmountKeyValue>
|
||||
<USDShower
|
||||
amountSats={props.amountSats}
|
||||
fee={props.reserve}
|
||||
@@ -170,7 +174,7 @@ export function AmountCard(props: {
|
||||
</Match>
|
||||
<Match when={!props.fee && !props.reserve}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key={i18n.t("receive.amount")}>
|
||||
<AmountKeyValue key={i18n.t("receive.amount")}>
|
||||
<Show
|
||||
when={props.isAmountEditable}
|
||||
fallback={
|
||||
@@ -195,7 +199,7 @@ export function AmountCard(props: {
|
||||
fee={props.fee}
|
||||
/>
|
||||
</Show>
|
||||
</KeyValue>
|
||||
</AmountKeyValue>
|
||||
<USDShower amountSats={props.amountSats} />
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
@@ -13,13 +13,19 @@ import {
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function LoadingShimmer() {
|
||||
export function LoadingShimmer(props: { center?: boolean }) {
|
||||
return (
|
||||
<div class="flex animate-pulse flex-col gap-2">
|
||||
<h1 class="text-4xl font-light">
|
||||
<h1
|
||||
class="text-4xl font-light"
|
||||
classList={{ "flex justify-center": props.center }}
|
||||
>
|
||||
<div class="h-[2.5rem] w-[12rem] rounded bg-neutral-700" />
|
||||
</h1>
|
||||
<h2 class="text-xl font-light text-white/70">
|
||||
<h2
|
||||
class="text-xl font-light text-white/70"
|
||||
classList={{ "flex justify-center": props.center }}
|
||||
>
|
||||
<div class="h-[1.75rem] w-[8rem] rounded bg-neutral-700" />
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -171,9 +171,9 @@ export function ContactViewer(props: {
|
||||
}
|
||||
>
|
||||
<KeyValue
|
||||
key={
|
||||
"Lightning Address"
|
||||
}
|
||||
key={i18n.t(
|
||||
"contacts.lightning_address"
|
||||
)}
|
||||
>
|
||||
<MiniStringShower
|
||||
text={
|
||||
|
||||
@@ -1,532 +0,0 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { MutinyChannel, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
For,
|
||||
Match,
|
||||
ParentComponent,
|
||||
Show,
|
||||
Suspense,
|
||||
Switch
|
||||
} from "solid-js";
|
||||
|
||||
import bolt from "~/assets/icons/bolt-black.svg";
|
||||
import chain from "~/assets/icons/chain-black.svg";
|
||||
import copyIcon from "~/assets/icons/copy.svg";
|
||||
import shuffle from "~/assets/icons/shuffle-black.svg";
|
||||
import {
|
||||
ActivityAmount,
|
||||
AmountSmall,
|
||||
CopyButton,
|
||||
ExternalLink,
|
||||
HackActivityType,
|
||||
Hr,
|
||||
InfoBox,
|
||||
ModalCloseButton,
|
||||
TinyButton,
|
||||
TruncateMiddle,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import {
|
||||
mempoolTxUrl,
|
||||
MutinyTagItem,
|
||||
prettyPrintTime,
|
||||
tagToMutinyTag,
|
||||
useCopy
|
||||
} from "~/utils";
|
||||
|
||||
interface ChannelClosure {
|
||||
channel_id: string;
|
||||
node_id: string;
|
||||
reason: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface OnChainTx {
|
||||
txid: string;
|
||||
received: number;
|
||||
sent: number;
|
||||
fee?: number;
|
||||
confirmation_time?: {
|
||||
Confirmed?: {
|
||||
height: number;
|
||||
time: number;
|
||||
};
|
||||
};
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||
export const DIALOG_POSITIONER =
|
||||
"fixed inset-0 z-50 flex items-center justify-center";
|
||||
export const DIALOG_CONTENT =
|
||||
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-m-grey-800/75 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
|
||||
|
||||
function LightningHeader(props: {
|
||||
info: MutinyInvoice;
|
||||
tags: MutinyTagItem[];
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
return (
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="rounded-full bg-neutral-100 p-4">
|
||||
<img src={bolt} alt="lightning bolt" class="h-8 w-8" />
|
||||
</div>
|
||||
<h1 class="font-semibold uppercase">
|
||||
{props.info.inbound
|
||||
? i18n.t("modals.transaction_details.lightning_receive")
|
||||
: i18n.t("modals.transaction_details.lightning_send")}
|
||||
</h1>
|
||||
<ActivityAmount
|
||||
center
|
||||
amount={props.info.amount_sats?.toString() ?? "0"}
|
||||
price={state.price}
|
||||
positive={props.info.inbound}
|
||||
/>
|
||||
<For each={props.tags}>
|
||||
{(tag) => (
|
||||
<TinyButton
|
||||
tag={tag}
|
||||
onClick={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
</TinyButton>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OnchainHeader(props: {
|
||||
info: OnChainTx;
|
||||
tags: MutinyTagItem[];
|
||||
kind?: HackActivityType;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const isSend = () => {
|
||||
return props.info.sent > props.info.received;
|
||||
};
|
||||
|
||||
const amount = () => {
|
||||
if (isSend()) {
|
||||
return (props.info.sent - props.info.received).toString();
|
||||
} else {
|
||||
return (props.info.received - props.info.sent).toString();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="rounded-full bg-neutral-100 p-4">
|
||||
<Switch>
|
||||
<Match
|
||||
when={
|
||||
props.kind === "ChannelOpen" ||
|
||||
props.kind === "ChannelClose"
|
||||
}
|
||||
>
|
||||
<img src={shuffle} alt="swap" class="h-8 w-8" />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={chain} alt="blockchain" class="h-8 w-8" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<h1 class="font-semibold uppercase">
|
||||
{props.kind === "ChannelOpen"
|
||||
? i18n.t("modals.transaction_details.channel_open")
|
||||
: props.kind === "ChannelClose"
|
||||
? i18n.t("modals.transaction_details.channel_close")
|
||||
: isSend()
|
||||
? i18n.t("modals.transaction_details.onchain_send")
|
||||
: i18n.t("modals.transaction_details.onchain_receive")}
|
||||
</h1>
|
||||
<Show when={props.kind !== "ChannelClose"}>
|
||||
<ActivityAmount
|
||||
center
|
||||
amount={amount() ?? "0"}
|
||||
price={state.price}
|
||||
positive={!isSend()}
|
||||
/>
|
||||
</Show>
|
||||
<For each={props.tags}>
|
||||
{(tag) => (
|
||||
<TinyButton
|
||||
tag={tag}
|
||||
onClick={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
</TinyButton>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
||||
return (
|
||||
<li class="flex items-center justify-between gap-4">
|
||||
<span class="whitespace-nowrap text-sm font-semibold uppercase">
|
||||
{props.key}
|
||||
</span>
|
||||
<span class="font-light">{props.children}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export function MiniStringShower(props: { text: string }) {
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
return (
|
||||
<div class="grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1">
|
||||
<TruncateMiddle text={props.text} />
|
||||
{/* <pre class="truncate text-neutral-300 font-light">{props.text}</pre> */}
|
||||
<button
|
||||
class="w-[1.5rem] p-1"
|
||||
classList={{ "bg-m-green rounded": copied() }}
|
||||
onClick={() => copy(props.text)}
|
||||
>
|
||||
<img src={copyIcon} alt="copy" class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LightningDetails(props: { info: MutinyInvoice }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<VStack>
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.status")}>
|
||||
<span class="text-neutral-300">
|
||||
{props.info.paid
|
||||
? i18n.t("modals.transaction_details.paid")
|
||||
: i18n.t("modals.transaction_details.unpaid")}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||
<span class="text-neutral-300">
|
||||
{prettyPrintTime(Number(props.info.last_updated))}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<Show when={props.info.description}>
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.description")}
|
||||
>
|
||||
<span class="truncate text-neutral-300">
|
||||
{props.info.description}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.fees")}>
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall amountSats={props.info.fees_paid} />
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.bolt11")}>
|
||||
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.payment_hash")}
|
||||
>
|
||||
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.preimage")}>
|
||||
<MiniStringShower text={props.info.preimage ?? ""} />
|
||||
</KeyValue>
|
||||
</ul>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const confirmationTime = () => {
|
||||
return props.info.confirmation_time?.Confirmed?.time;
|
||||
};
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
// Can return nothing if the channel is already closed
|
||||
const [channelInfo] = createResource(async () => {
|
||||
if (props.kind === "ChannelOpen") {
|
||||
try {
|
||||
const channels =
|
||||
await (state.mutiny_wallet?.list_channels() as Promise<
|
||||
MutinyChannel[]
|
||||
>);
|
||||
const channel = channels.find(
|
||||
(channel) => channel.outpoint?.startsWith(props.info.txid)
|
||||
);
|
||||
return channel;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.status")}>
|
||||
<span class="text-neutral-300">
|
||||
{confirmationTime()
|
||||
? i18n.t("modals.transaction_details.confirmed")
|
||||
: i18n.t("modals.transaction_details.unconfirmed")}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<Show when={confirmationTime()}>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||
<span class="text-neutral-300">
|
||||
{confirmationTime()
|
||||
? prettyPrintTime(Number(confirmationTime()))
|
||||
: "Pending"}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<Show when={props.info.fee && props.info.fee > 0}>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.fee")}>
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall amountSats={props.info.fee} />
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.txid")}>
|
||||
<MiniStringShower text={props.info.txid ?? ""} />
|
||||
</KeyValue>
|
||||
<Switch>
|
||||
<Match when={props.kind === "ChannelOpen" && channelInfo()}>
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.balance")}
|
||||
>
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall
|
||||
amountSats={channelInfo()?.balance}
|
||||
/>
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.reserve")}
|
||||
>
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall
|
||||
amountSats={channelInfo()?.reserve}
|
||||
/>
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.peer")}
|
||||
>
|
||||
<span class="text-neutral-300">
|
||||
<MiniStringShower
|
||||
text={channelInfo()?.peer ?? ""}
|
||||
/>
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Match>
|
||||
<Match when={props.kind === "ChannelOpen"}>
|
||||
<InfoBox accent="blue">
|
||||
{i18n.t("modals.transaction_details.no_details")}
|
||||
</InfoBox>
|
||||
</Match>
|
||||
</Switch>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<ExternalLink href={mempoolTxUrl(props.info.txid, network)}>
|
||||
{i18n.t("common.view_transaction")}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelCloseDetails(props: { info: ChannelClosure }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<VStack>
|
||||
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.channel_id")}>
|
||||
<MiniStringShower text={props.info.channel_id ?? ""} />
|
||||
</KeyValue>
|
||||
<Show when={props.info.timestamp}>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||
<span class="text-neutral-300">
|
||||
{props.info.timestamp
|
||||
? prettyPrintTime(Number(props.info.timestamp))
|
||||
: i18n.t("common.pending")}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key={i18n.t("modals.transaction_details.reason")}>
|
||||
<p class="text-right text-neutral-300">
|
||||
{props.info.reason ?? ""}
|
||||
</p>
|
||||
</KeyValue>
|
||||
</ul>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailsIdModal(props: {
|
||||
open: boolean;
|
||||
kind?: HackActivityType;
|
||||
id: string;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const id = () => props.id;
|
||||
const kind = () => props.kind;
|
||||
|
||||
// TODO: is there a cleaner way to do refetch when id changes?
|
||||
const [data, { refetch }] = createResource(async () => {
|
||||
try {
|
||||
if (kind() === "Lightning") {
|
||||
console.debug("reading invoice: ", id());
|
||||
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(
|
||||
id()
|
||||
);
|
||||
return invoice;
|
||||
} else if (kind() === "ChannelClose") {
|
||||
console.debug("reading channel close: ", id());
|
||||
const closeItem =
|
||||
await state.mutiny_wallet?.get_channel_closure(id());
|
||||
|
||||
return closeItem;
|
||||
} else {
|
||||
console.debug("reading tx: ", id());
|
||||
const tx = await state.mutiny_wallet?.get_transaction(id());
|
||||
|
||||
return tx;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const tags = createMemo(() => {
|
||||
if (data() && data().labels && data().labels.length > 0) {
|
||||
try {
|
||||
const contact = state.mutiny_wallet?.get_contact(
|
||||
data().labels[0]
|
||||
);
|
||||
if (contact) {
|
||||
return [tagToMutinyTag(contact)];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (props.id && props.kind && props.open) {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
const json = createMemo(() => JSON.stringify(data() || "", null, 2));
|
||||
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class={OVERLAY} />
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<Suspense>
|
||||
<div class="mb-2 flex justify-between">
|
||||
<div />
|
||||
<Dialog.CloseButton>
|
||||
<ModalCloseButton />
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Title>
|
||||
<Switch>
|
||||
<Match when={props.kind === "Lightning"}>
|
||||
<LightningHeader
|
||||
info={data() as MutinyInvoice}
|
||||
tags={tags()}
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
props.kind === "OnChain" ||
|
||||
props.kind === "ChannelOpen" ||
|
||||
props.kind === "ChannelClose"
|
||||
}
|
||||
>
|
||||
<OnchainHeader
|
||||
info={data() as OnChainTx}
|
||||
tags={tags()}
|
||||
kind={props.kind}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Dialog.Title>
|
||||
<Hr />
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<Switch>
|
||||
<Match when={props.kind === "Lightning"}>
|
||||
<LightningDetails
|
||||
info={data() as MutinyInvoice}
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
props.kind === "OnChain" ||
|
||||
props.kind === "ChannelOpen"
|
||||
}
|
||||
>
|
||||
<OnchainDetails
|
||||
info={data() as OnChainTx}
|
||||
kind={props.kind}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.kind === "ChannelClose"}>
|
||||
<ChannelCloseDetails
|
||||
info={data() as ChannelClosure}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={props.kind !== "ChannelClose"}>
|
||||
<div class="flex justify-center">
|
||||
<CopyButton
|
||||
title={i18n.t("common.copy")}
|
||||
text={json()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</Dialog.Description>
|
||||
</Suspense>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -46,8 +46,7 @@ export function TruncateMiddle(props: { text: string; whiteBg?: boolean }) {
|
||||
<div
|
||||
class="flex font-mono"
|
||||
classList={{
|
||||
"text-black": props.whiteBg,
|
||||
"text-neutral-300": !props.whiteBg
|
||||
"text-black": props.whiteBg
|
||||
}}
|
||||
>
|
||||
<span class="truncate">{props.text}</span>
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from "./layout";
|
||||
export * from "./successfail";
|
||||
|
||||
export * from "./Activity";
|
||||
export * from "./ActivityDetailsModal";
|
||||
export * from "./ActivityItem";
|
||||
export * from "./Amount";
|
||||
export * from "./AmountCard";
|
||||
@@ -14,7 +15,6 @@ export * from "./ContactForm";
|
||||
export * from "./ContactViewer";
|
||||
export * from "./DecryptDialog";
|
||||
export * from "./DeleteEverything";
|
||||
export * from "./DetailsModal";
|
||||
export * from "./ErrorDisplay";
|
||||
export * from "./Fee";
|
||||
export * from "./I18nProvider";
|
||||
|
||||
@@ -21,7 +21,11 @@ export function BackPop() {
|
||||
|
||||
return (
|
||||
<BackButton
|
||||
title={i18n.t("common.back")}
|
||||
title={
|
||||
backPath() === "/"
|
||||
? i18n.t("common.home")
|
||||
: i18n.t("common.back")
|
||||
}
|
||||
onClick={() => navigate(backPath())}
|
||||
showOnDesktop
|
||||
/>
|
||||
|
||||
@@ -184,6 +184,17 @@ export const MutinyWalletGuard: ParentComponent = (props) => {
|
||||
|
||||
export const Hr = () => <Separator.Root class="my-4 border-m-grey-750" />;
|
||||
|
||||
export const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
||||
return (
|
||||
<li class="flex items-center justify-between gap-6">
|
||||
<span class="min-w-max text-sm font-semibold uppercase text-m-grey-400">
|
||||
{props.key}
|
||||
</span>
|
||||
<span class="truncate font-light">{props.children}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const LargeHeader: ParentComponent<{
|
||||
action?: JSX.Element;
|
||||
centered?: boolean;
|
||||
|
||||
@@ -17,7 +17,8 @@ export default {
|
||||
error_unimplemented: "Unimplemented",
|
||||
why: "Why?",
|
||||
private_tags: "Private tags",
|
||||
view_transaction: "View Transaction",
|
||||
view_transaction: "View transaction",
|
||||
view_payment_details: "View payment details",
|
||||
pending: "Pending",
|
||||
error_safe_mode:
|
||||
"Mutiny is running in safe mode. Lightning is disabled.",
|
||||
@@ -36,6 +37,7 @@ export default {
|
||||
pay: "Pay",
|
||||
name: "Name",
|
||||
placeholder: "Satoshi",
|
||||
lightning_address: "Lightning Address",
|
||||
unimplemented: "Unimplemented",
|
||||
not_available: "We don't do that yet",
|
||||
error_name: "We at least need a name"
|
||||
@@ -163,7 +165,40 @@ export default {
|
||||
coming_soon: "Coming soon",
|
||||
private: "Private",
|
||||
anonymous: "Anonymous",
|
||||
from: "From:"
|
||||
from: "From:",
|
||||
transaction_details: {
|
||||
lightning_receive: "Received via Lightning",
|
||||
lightning_send: "Sent via Lightning",
|
||||
channel_open: "Channel open",
|
||||
channel_close: "Channel close",
|
||||
onchain_receive: "On-chain receive",
|
||||
onchain_send: "On-chain send",
|
||||
paid: "Paid",
|
||||
unpaid: "Unpaid",
|
||||
status: "Status",
|
||||
date: "Date",
|
||||
tagged_to: "Tagged to",
|
||||
description: "Description",
|
||||
fee: "Fee",
|
||||
onchain_fee: "On-chain Fee",
|
||||
invoice: "Invoice",
|
||||
payment_hash: "Payment Hash",
|
||||
payment_preimage: "Preimage",
|
||||
txid: "Txid",
|
||||
total: "Amount Requested",
|
||||
balance: "Balance",
|
||||
reserve: "Reserve",
|
||||
peer: "Peer",
|
||||
channel_id: "Channel ID",
|
||||
reason: "Reason",
|
||||
confirmed: "Confirmed",
|
||||
unconfirmed: "Unconfirmed",
|
||||
sweep_delay:
|
||||
"Funds may take a few days to be swept back into the wallet",
|
||||
no_details:
|
||||
"No channel details found, which means this channel has likely been closed.",
|
||||
back_home: "back home"
|
||||
}
|
||||
},
|
||||
redshift: {
|
||||
title: "Redshift",
|
||||
@@ -625,34 +660,6 @@ export default {
|
||||
"If you want to use pretend money to test out Mutiny without risk,",
|
||||
signet_link: "check out our Signet version."
|
||||
},
|
||||
transaction_details: {
|
||||
lightning_receive: "Lightning receive",
|
||||
lightning_send: "Lightning send",
|
||||
channel_open: "Channel open",
|
||||
channel_close: "Channel close",
|
||||
onchain_receive: "On-chain receive",
|
||||
onchain_send: "On-chain send",
|
||||
paid: "Paid",
|
||||
unpaid: "Unpaid",
|
||||
status: "Status",
|
||||
when: "When",
|
||||
description: "Description",
|
||||
fee: "Fee",
|
||||
fees: "Fees",
|
||||
bolt11: "Bolt11",
|
||||
payment_hash: "Payment Hash",
|
||||
preimage: "Preimage",
|
||||
txid: "Txid",
|
||||
balance: "Balance",
|
||||
reserve: "Reserve",
|
||||
peer: "Peer",
|
||||
channel_id: "Channel ID",
|
||||
reason: "Reason",
|
||||
confirmed: "Confirmed",
|
||||
unconfirmed: "Unconfirmed",
|
||||
no_details:
|
||||
"No channel details found, which means this channel has likely been closed."
|
||||
},
|
||||
more_info: {
|
||||
whats_with_the_fees: "What's with the fees?",
|
||||
self_custodial:
|
||||
|
||||
@@ -145,7 +145,35 @@ export default {
|
||||
unknown: "알 수 없음",
|
||||
import_contacts:
|
||||
"Nostr에서 연락처를 가져와 누가 체널을 열고 있는지 확인하세요.",
|
||||
coming_soon: "곧 출시 예정"
|
||||
coming_soon: "곧 출시 예정",
|
||||
transaction_details: {
|
||||
lightning_receive: "라이트닝 입금",
|
||||
lightning_send: "라이트닝 송금",
|
||||
channel_open: "채널 개설",
|
||||
channel_close: "채널 종료",
|
||||
onchain_receive: "체인상 입금",
|
||||
onchain_send: "체인상 송금",
|
||||
paid: "지불 완료",
|
||||
unpaid: "미지불",
|
||||
status: "상태",
|
||||
when: "시간",
|
||||
description: "설명",
|
||||
fee: "수수료",
|
||||
fees: "수수료",
|
||||
bolt11: "Bolt11",
|
||||
payment_hash: "지불 해시",
|
||||
preimage: "사전 이미지",
|
||||
txid: "거래 ID",
|
||||
balance: "잔고",
|
||||
reserve: "리저브",
|
||||
peer: "피어",
|
||||
channel_id: "채널 ID",
|
||||
reason: "이유",
|
||||
confirmed: "확인됨",
|
||||
unconfirmed: "확인 대기",
|
||||
no_details:
|
||||
"채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다."
|
||||
}
|
||||
},
|
||||
redshift: {
|
||||
title: "레드시프트",
|
||||
@@ -497,34 +525,6 @@ export default {
|
||||
"위험 없이 Mutiny를 테스트하려면 가상 자금을 사용하려면",
|
||||
signet_link: "Signet 버전을 확인하세요."
|
||||
},
|
||||
transaction_details: {
|
||||
lightning_receive: "라이트닝 입금",
|
||||
lightning_send: "라이트닝 송금",
|
||||
channel_open: "채널 개설",
|
||||
channel_close: "채널 종료",
|
||||
onchain_receive: "체인상 입금",
|
||||
onchain_send: "체인상 송금",
|
||||
paid: "지불 완료",
|
||||
unpaid: "미지불",
|
||||
status: "상태",
|
||||
when: "시간",
|
||||
description: "설명",
|
||||
fee: "수수료",
|
||||
fees: "수수료",
|
||||
bolt11: "Bolt11",
|
||||
payment_hash: "지불 해시",
|
||||
preimage: "사전 이미지",
|
||||
txid: "거래 ID",
|
||||
balance: "잔고",
|
||||
reserve: "리저브",
|
||||
peer: "피어",
|
||||
channel_id: "채널 ID",
|
||||
reason: "이유",
|
||||
confirmed: "확인됨",
|
||||
unconfirmed: "확인 대기",
|
||||
no_details:
|
||||
"채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다."
|
||||
},
|
||||
more_info: {
|
||||
whats_with_the_fees: "수수료는 어떻게 되나요?",
|
||||
self_custodial:
|
||||
|
||||
@@ -29,7 +29,7 @@ html {
|
||||
}
|
||||
|
||||
a {
|
||||
@apply underline decoration-light-text hover:decoration-white;
|
||||
@apply underline decoration-m-grey-400 hover:decoration-white;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useNavigate } from "solid-start";
|
||||
|
||||
import side2side from "~/assets/icons/side-to-side.svg";
|
||||
import {
|
||||
ActivityDetailsModal,
|
||||
AmountCard,
|
||||
AmountFiat,
|
||||
AmountSats,
|
||||
@@ -26,9 +27,9 @@ import {
|
||||
Card,
|
||||
Checkbox,
|
||||
DefaultMain,
|
||||
ExternalLink,
|
||||
Fee,
|
||||
FeesModal,
|
||||
HackActivityType,
|
||||
Indicator,
|
||||
InfoBox,
|
||||
IntegratedQr,
|
||||
@@ -45,11 +46,9 @@ import {
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import {
|
||||
eify,
|
||||
mempoolTxUrl,
|
||||
MutinyTagItem,
|
||||
objectToSearchParams,
|
||||
vibrateSuccess
|
||||
@@ -142,6 +141,11 @@ export default function Receive() {
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [error, setError] = createSignal<string>("");
|
||||
|
||||
// Details Modal
|
||||
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
||||
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
|
||||
const [detailsId, setDetailsId] = createSignal<string>("");
|
||||
|
||||
const RECEIVE_FLAVORS = [
|
||||
{
|
||||
value: "unified",
|
||||
@@ -184,6 +188,30 @@ export default function Receive() {
|
||||
setSelectedValues([]);
|
||||
}
|
||||
|
||||
function openDetailsModal() {
|
||||
const paymentTxId =
|
||||
paidState() === "onchain_paid"
|
||||
? paymentTx()
|
||||
? paymentTx()?.txid
|
||||
: undefined
|
||||
: paymentInvoice()
|
||||
? paymentInvoice()?.payment_hash
|
||||
: undefined;
|
||||
const kind = paidState() === "onchain_paid" ? "OnChain" : "Lightning";
|
||||
|
||||
console.log("Opening details modal: ", paymentTxId, kind);
|
||||
|
||||
if (!paymentTxId) {
|
||||
console.warn("No id provided to openDetailsModal");
|
||||
return;
|
||||
}
|
||||
if (paymentTxId !== undefined) {
|
||||
setDetailsId(paymentTxId);
|
||||
}
|
||||
setDetailsKind(kind);
|
||||
setDetailsOpen(true);
|
||||
}
|
||||
|
||||
async function processContacts(
|
||||
contacts: Partial<MutinyTagItem>[]
|
||||
): Promise<string[]> {
|
||||
@@ -346,8 +374,6 @@ export default function Receive() {
|
||||
|
||||
const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid);
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
createEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (receiveState() === "show") refetch();
|
||||
@@ -483,6 +509,14 @@ export default function Receive() {
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
<Show when={detailsId() && detailsKind()}>
|
||||
<ActivityDetailsModal
|
||||
open={detailsOpen()}
|
||||
kind={detailsKind()}
|
||||
id={detailsId()}
|
||||
setOpen={setDetailsOpen}
|
||||
/>
|
||||
</Show>
|
||||
<MegaCheck />
|
||||
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
|
||||
{receiveState() === "paid" &&
|
||||
@@ -525,22 +559,14 @@ export default function Receive() {
|
||||
>
|
||||
<Fee amountSats={lspFee()} />
|
||||
</Show>
|
||||
{/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first
|
||||
{/*TODO: add internal payment detail page* for lightning*/}
|
||||
<Show
|
||||
when={
|
||||
receiveState() === "paid" &&
|
||||
paidState() === "onchain_paid"
|
||||
}
|
||||
>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
paymentTx()?.txid,
|
||||
network
|
||||
)}
|
||||
{/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first*/}
|
||||
<Show when={receiveState() === "paid"}>
|
||||
<p
|
||||
class="cursor-pointer underline"
|
||||
onClick={openDetailsModal}
|
||||
>
|
||||
{i18n.t("common.view_transaction")}
|
||||
</ExternalLink>
|
||||
{i18n.t("common.view_payment_details")}
|
||||
</p>
|
||||
</Show>
|
||||
</SuccessModal>
|
||||
</Match>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useNavigate } from "solid-start";
|
||||
import { Paste } from "~/assets/svg/Paste";
|
||||
import { Scan } from "~/assets/svg/Scan";
|
||||
import {
|
||||
ActivityDetailsModal,
|
||||
AmountCard,
|
||||
AmountFiat,
|
||||
AmountSats,
|
||||
@@ -24,9 +25,9 @@ import {
|
||||
ButtonLink,
|
||||
Card,
|
||||
DefaultMain,
|
||||
ExternalLink,
|
||||
Fee,
|
||||
GiftLink,
|
||||
HackActivityType,
|
||||
HStack,
|
||||
InfoBox,
|
||||
LargeHeader,
|
||||
@@ -44,10 +45,9 @@ import {
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { ParsedParams } from "~/logic/waila";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { eify, mempoolTxUrl, MutinyTagItem, vibrateSuccess } from "~/utils";
|
||||
import { eify, MutinyTagItem, vibrateSuccess } from "~/utils";
|
||||
|
||||
export type SendSource = "lightning" | "onchain";
|
||||
|
||||
@@ -59,6 +59,7 @@ type SentDetails = {
|
||||
amount?: bigint;
|
||||
destination?: string;
|
||||
txid?: string;
|
||||
payment_hash?: string;
|
||||
failure_reason?: string;
|
||||
fee_estimate?: bigint | number;
|
||||
};
|
||||
@@ -227,6 +228,11 @@ export default function Send() {
|
||||
Partial<MutinyTagItem>[]
|
||||
>([]);
|
||||
|
||||
// Details Modal
|
||||
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
||||
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
|
||||
const [detailsId, setDetailsId] = createSignal("");
|
||||
|
||||
// Errors
|
||||
const [error, setError] = createSignal<string>();
|
||||
|
||||
@@ -243,6 +249,29 @@ export default function Send() {
|
||||
setFieldDestination("");
|
||||
}
|
||||
|
||||
function openDetailsModal() {
|
||||
const paymentTxId = sentDetails()?.txid
|
||||
? sentDetails()
|
||||
? sentDetails()?.txid
|
||||
: undefined
|
||||
: sentDetails()
|
||||
? sentDetails()?.payment_hash
|
||||
: undefined;
|
||||
const kind = sentDetails()?.txid ? "OnChain" : "Lightning";
|
||||
|
||||
console.log("Opening details modal: ", paymentTxId, kind);
|
||||
|
||||
if (!paymentTxId) {
|
||||
console.warn("No id provided to openDetailsModal");
|
||||
return;
|
||||
}
|
||||
if (paymentTxId !== undefined) {
|
||||
setDetailsId(paymentTxId);
|
||||
}
|
||||
setDetailsKind(kind);
|
||||
setDetailsOpen(true);
|
||||
}
|
||||
|
||||
// If we got here from a scan result we want to set the destination and clean up that scan result
|
||||
onMount(() => {
|
||||
if (state.scan_result) {
|
||||
@@ -478,6 +507,7 @@ export default function Send() {
|
||||
tags
|
||||
);
|
||||
sentDetails.amount = invoice()?.amount_sats;
|
||||
sentDetails.payment_hash = invoice()?.payment_hash;
|
||||
sentDetails.fee_estimate = payment?.fees_paid || 0;
|
||||
} else {
|
||||
const payment = await state.mutiny_wallet?.pay_invoice(
|
||||
@@ -487,6 +517,7 @@ export default function Send() {
|
||||
tags
|
||||
);
|
||||
sentDetails.amount = amountSats();
|
||||
sentDetails.payment_hash = invoice()?.payment_hash;
|
||||
sentDetails.fee_estimate = payment?.fees_paid || 0;
|
||||
}
|
||||
} else if (source() === "lightning" && nodePubkey()) {
|
||||
@@ -505,6 +536,7 @@ export default function Send() {
|
||||
throw new Error(i18n.t("send.error_keysend"));
|
||||
} else {
|
||||
sentDetails.amount = amountSats();
|
||||
sentDetails.payment_hash = invoice()?.payment_hash;
|
||||
sentDetails.fee_estimate = payment?.fees_paid || 0;
|
||||
}
|
||||
} else if (source() === "lightning" && lnurlp()) {
|
||||
@@ -517,11 +549,13 @@ export default function Send() {
|
||||
undefined, // zap_npub
|
||||
tags
|
||||
);
|
||||
sentDetails.payment_hash = invoice()?.payment_hash;
|
||||
|
||||
if (!payment?.paid) {
|
||||
throw new Error(i18n.t("send.error_LNURL"));
|
||||
} else {
|
||||
sentDetails.amount = amountSats();
|
||||
sentDetails.payment_hash = invoice()?.payment_hash;
|
||||
sentDetails.fee_estimate = payment?.fees_paid || 0;
|
||||
}
|
||||
} else if (source() === "onchain" && address()) {
|
||||
@@ -568,8 +602,6 @@ export default function Send() {
|
||||
return !destination() || sending() || amountSats() === 0n || !!error();
|
||||
});
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<SafeArea>
|
||||
@@ -614,6 +646,14 @@ export default function Send() {
|
||||
{/*TODO: add failure hint logic for different failure conditions*/}
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Show when={detailsId() && detailsKind()}>
|
||||
<ActivityDetailsModal
|
||||
open={detailsOpen()}
|
||||
kind={detailsKind()}
|
||||
id={detailsId()}
|
||||
setOpen={setDetailsOpen}
|
||||
/>
|
||||
</Show>
|
||||
<MegaCheck />
|
||||
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
|
||||
{sentDetails()?.amount
|
||||
@@ -638,16 +678,12 @@ export default function Send() {
|
||||
</div>
|
||||
<hr class="w-16 bg-m-grey-400" />
|
||||
<Fee amountSats={sentDetails()?.fee_estimate} />
|
||||
<Show when={sentDetails()?.txid}>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
sentDetails()?.txid,
|
||||
network
|
||||
)}
|
||||
>
|
||||
{i18n.t("common.view_transaction")}
|
||||
</ExternalLink>
|
||||
</Show>
|
||||
<p
|
||||
class="cursor-pointer underline"
|
||||
onClick={openDetailsModal}
|
||||
>
|
||||
{i18n.t("common.view_payment_details")}
|
||||
</p>
|
||||
</Match>
|
||||
</Switch>
|
||||
</SuccessModal>
|
||||
|
||||
@@ -12,13 +12,14 @@ import {
|
||||
import { useNavigate } from "solid-start";
|
||||
|
||||
import {
|
||||
ActivityDetailsModal,
|
||||
AmountCard,
|
||||
AmountFiat,
|
||||
BackLink,
|
||||
Button,
|
||||
Card,
|
||||
DefaultMain,
|
||||
ExternalLink,
|
||||
HackActivityType,
|
||||
InfoBox,
|
||||
LargeHeader,
|
||||
MegaCheck,
|
||||
@@ -35,7 +36,7 @@ import { useI18n } from "~/i18n/context";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { MethodChooser, SendSource } from "~/routes/Send";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { eify, mempoolTxUrl, vibrateSuccess } from "~/utils";
|
||||
import { eify, vibrateSuccess } from "~/utils";
|
||||
|
||||
const CHANNEL_FEE_ESTIMATE_ADDRESS =
|
||||
"bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf";
|
||||
@@ -62,9 +63,32 @@ export default function Swap() {
|
||||
|
||||
const [selectedPeer, setSelectedPeer] = createSignal<string>("");
|
||||
|
||||
// Details Modal
|
||||
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
||||
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
|
||||
const [detailsId, setDetailsId] = createSignal("");
|
||||
|
||||
const [channelOpenResult, setChannelOpenResult] =
|
||||
createSignal<ChannelOpenDetails>();
|
||||
|
||||
function openDetailsModal() {
|
||||
const paymentTxId =
|
||||
channelOpenResult()?.channel?.outpoint?.split(":")[0];
|
||||
const kind: HackActivityType = "ChannelOpen";
|
||||
|
||||
console.log("Opening details modal: ", paymentTxId, kind);
|
||||
|
||||
if (!paymentTxId) {
|
||||
console.warn("No id provided to openDetailsModal");
|
||||
return;
|
||||
}
|
||||
if (paymentTxId !== undefined) {
|
||||
setDetailsId(paymentTxId);
|
||||
}
|
||||
setDetailsKind(kind);
|
||||
setDetailsOpen(true);
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
setSource("onchain");
|
||||
setAmountSats(0n);
|
||||
@@ -258,8 +282,6 @@ export default function Swap() {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<SafeArea>
|
||||
@@ -293,6 +315,14 @@ export default function Swap() {
|
||||
{/*TODO: Error hint needs to be added for possible failure reasons*/}
|
||||
</Match>
|
||||
<Match when={channelOpenResult()?.channel}>
|
||||
<Show when={detailsId() && detailsKind()}>
|
||||
<ActivityDetailsModal
|
||||
open={detailsOpen()}
|
||||
kind={detailsKind()}
|
||||
id={detailsId()}
|
||||
setOpen={setDetailsOpen}
|
||||
/>
|
||||
</Show>
|
||||
<MegaCheck />
|
||||
<div class="flex flex-col justify-center">
|
||||
<h1 class="mb-2 mt-4 w-full justify-center text-center text-2xl font-semibold md:text-3xl">
|
||||
@@ -328,22 +358,12 @@ export default function Swap() {
|
||||
</div>
|
||||
</div>
|
||||
<hr class="w-16 bg-m-grey-400" />
|
||||
<Show
|
||||
when={
|
||||
channelOpenResult()?.channel?.outpoint
|
||||
}
|
||||
<p
|
||||
class="cursor-pointer underline"
|
||||
onClick={openDetailsModal}
|
||||
>
|
||||
<ExternalLink
|
||||
href={mempoolTxUrl(
|
||||
channelOpenResult()?.channel?.outpoint?.split(
|
||||
":"
|
||||
)[0],
|
||||
network
|
||||
)}
|
||||
>
|
||||
{i18n.t("common.view_transaction")}
|
||||
</ExternalLink>
|
||||
</Show>
|
||||
{i18n.t("common.view_payment_details")}
|
||||
</p>
|
||||
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
function BalanceBar(props: {
|
||||
export function BalanceBar(props: {
|
||||
inbound: number;
|
||||
reserve: number;
|
||||
outbound: number;
|
||||
|
||||
Reference in New Issue
Block a user