mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-01-09 17:24:34 +01:00
move types from Activity component
This commit is contained in:
161
src/components/Activity.tsx
Normal file
161
src/components/Activity.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal
|
||||
} from "solid-js";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||
import { A } from "solid-start";
|
||||
import { createDeepSignal } from "~/utils/deepSignal";
|
||||
import {
|
||||
NiceP,
|
||||
DetailsIdModal,
|
||||
LoadingShimmer,
|
||||
ActivityItem,
|
||||
HackActivityType
|
||||
} from "~/components";
|
||||
|
||||
export const THREE_COLUMNS =
|
||||
"grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0";
|
||||
export const CENTER_COLUMN = "min-w-0 overflow-hidden max-w-full";
|
||||
export const MISSING_LABEL =
|
||||
"py-1 px-2 bg-white/10 rounded inline-block text-sm";
|
||||
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 {
|
||||
kind: HackActivityType;
|
||||
id: string;
|
||||
amount_sats: number;
|
||||
inbound: boolean;
|
||||
labels: string[];
|
||||
contacts: Contact[];
|
||||
last_updated: number;
|
||||
}
|
||||
|
||||
function UnifiedActivityItem(props: {
|
||||
item: IActivityItem;
|
||||
onClick: (id: string, kind: HackActivityType) => void;
|
||||
}) {
|
||||
const click = () => {
|
||||
props.onClick(
|
||||
props.item.id,
|
||||
props.item.kind as unknown as HackActivityType
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ActivityItem
|
||||
// This is actually the ActivityType enum but wasm is hard
|
||||
kind={props.item.kind as unknown as HackActivityType}
|
||||
labels={props.item.labels}
|
||||
contacts={props.item.contacts}
|
||||
// FIXME: is this something we can put into node logic?
|
||||
amount={props.item.amount_sats || 0}
|
||||
date={props.item.last_updated}
|
||||
positive={props.item.inbound}
|
||||
onClick={click}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CombinedActivity(props: { limit?: number }) {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
||||
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
|
||||
const [detailsId, setDetailsId] = createSignal("");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setDetailsId(id);
|
||||
setDetailsKind(kind);
|
||||
setDetailsOpen(true);
|
||||
}
|
||||
|
||||
async function fetchActivity() {
|
||||
return await state.mutiny_wallet?.get_activity();
|
||||
}
|
||||
|
||||
const [activity, { refetch }] = createResource(fetchActivity, {
|
||||
storage: createDeepSignal
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
// Should re-run after every sync
|
||||
if (!state.is_syncing) {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={activity.state === "ready" || activity.state === "refreshing"}
|
||||
fallback={<LoadingShimmer />}
|
||||
>
|
||||
<Show when={detailsId() && detailsKind()}>
|
||||
<DetailsIdModal
|
||||
open={detailsOpen()}
|
||||
kind={detailsKind()}
|
||||
id={detailsId()}
|
||||
setOpen={setDetailsOpen}
|
||||
/>
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={activity.latest.length === 0}>
|
||||
<div class="w-full text-center pb-4">
|
||||
<NiceP>
|
||||
{i18n.t(
|
||||
"activity.receive_some_sats_to_get_started"
|
||||
)}
|
||||
</NiceP>
|
||||
</div>
|
||||
</Match>
|
||||
<Match
|
||||
when={props.limit && activity.latest.length > props.limit}
|
||||
>
|
||||
<For each={activity.latest.slice(0, props.limit)}>
|
||||
{(activityItem) => (
|
||||
<UnifiedActivityItem
|
||||
item={activityItem}
|
||||
onClick={openDetailsModal}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={activity.latest.length >= 0}>
|
||||
<For each={activity.latest}>
|
||||
{(activityItem) => (
|
||||
<UnifiedActivityItem
|
||||
item={activityItem}
|
||||
onClick={openDetailsModal}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={props.limit && activity.latest.length > 0}>
|
||||
<A
|
||||
href="/activity"
|
||||
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
|
||||
>
|
||||
{i18n.t("activity.view_all")}
|
||||
</A>
|
||||
</Show>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
530
src/components/DetailsModal.tsx
Normal file
530
src/components/DetailsModal.tsx
Normal file
@@ -0,0 +1,530 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
ParentComponent,
|
||||
Show,
|
||||
Suspense,
|
||||
Switch,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource
|
||||
} from "solid-js";
|
||||
import {
|
||||
InfoBox,
|
||||
Hr,
|
||||
ModalCloseButton,
|
||||
TinyButton,
|
||||
VStack,
|
||||
ActivityAmount,
|
||||
HackActivityType,
|
||||
CopyButton,
|
||||
TruncateMiddle,
|
||||
AmountSmall
|
||||
} from "~/components";
|
||||
import { MutinyChannel, MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||
|
||||
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 { prettyPrintTime } from "~/utils/prettyPrintTime";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { MutinyTagItem, tagToMutinyTag } from "~/utils/tags";
|
||||
import { useCopy } from "~/utils/useCopy";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { ExternalLink } from "@mutinywallet/ui";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
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-neutral-800/80 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="p-4 bg-neutral-100 rounded-full">
|
||||
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="uppercase font-semibold">
|
||||
{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="p-4 bg-neutral-100 rounded-full">
|
||||
<Switch>
|
||||
<Match
|
||||
when={
|
||||
props.kind === "ChannelOpen" ||
|
||||
props.kind === "ChannelClose"
|
||||
}
|
||||
>
|
||||
<img src={shuffle} alt="swap" class="w-8 h-8" />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={chain} alt="blockchain" class="w-8 h-8" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<h1 class="uppercase font-semibold">
|
||||
{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 justify-between items-center gap-4">
|
||||
<span class="uppercase font-semibold whitespace-nowrap text-sm">
|
||||
{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="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
|
||||
<TruncateMiddle text={props.text} />
|
||||
{/* <pre class="truncate text-neutral-300 font-light">{props.text}</pre> */}
|
||||
<button
|
||||
class="w-[1.5rem] p-1"
|
||||
classList={{ "bg-m-green rounded": copied() }}
|
||||
onClick={() => copy(props.text)}
|
||||
>
|
||||
<img src={copyIcon} alt="copy" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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="text-neutral-300 truncate">
|
||||
{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-neutral-300 text-right">
|
||||
{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="flex justify-between mb-2">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
652
src/routes/Redshift.tsx
Normal file
652
src/routes/Redshift.tsx
Normal file
@@ -0,0 +1,652 @@
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onMount,
|
||||
ParentComponent,
|
||||
Show,
|
||||
Suspense,
|
||||
Switch
|
||||
} from "solid-js";
|
||||
import {
|
||||
CENTER_COLUMN,
|
||||
MISSING_LABEL,
|
||||
REDSHIFT_LABEL,
|
||||
RIGHT_COLUMN,
|
||||
THREE_COLUMNS,
|
||||
Card,
|
||||
DefaultMain,
|
||||
LargeHeader,
|
||||
NiceP,
|
||||
MutinyWalletGuard,
|
||||
SafeArea,
|
||||
SmallAmount,
|
||||
SmallHeader,
|
||||
VStack,
|
||||
BackLink,
|
||||
StyledRadioGroup,
|
||||
NavBar,
|
||||
Button,
|
||||
ProgressBar,
|
||||
AmountSats
|
||||
} from "~/components";
|
||||
import { LoadingSpinner } from "@mutinywallet/ui";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import wave from "~/assets/wave.gif";
|
||||
import utxoIcon from "~/assets/icons/coin.svg";
|
||||
import { MutinyChannel } from "@mutinywallet/mutiny-wasm";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { getRedshifted, setRedshifted } from "~/utils/fakeLabels";
|
||||
|
||||
type ShiftOption = "utxo" | "lightning";
|
||||
|
||||
type ShiftStage = "choose" | "observe" | "success" | "failure";
|
||||
|
||||
type OutPoint = string; // Replace with the actual TypeScript type for OutPoint
|
||||
type RedshiftStatus = string; // Replace with the actual TypeScript type for RedshiftStatus
|
||||
type RedshiftRecipient = unknown; // Replace with the actual TypeScript type for RedshiftRecipient
|
||||
type PublicKey = unknown; // Replace with the actual TypeScript type for PublicKey
|
||||
|
||||
interface RedshiftResult {
|
||||
id: string;
|
||||
input_utxo: OutPoint;
|
||||
status: RedshiftStatus;
|
||||
recipient: RedshiftRecipient;
|
||||
output_utxo?: OutPoint;
|
||||
introduction_channel?: OutPoint;
|
||||
output_channel?: OutPoint;
|
||||
introduction_node: PublicKey;
|
||||
amount_sats: bigint;
|
||||
change_amt?: bigint;
|
||||
fees_paid: bigint;
|
||||
}
|
||||
|
||||
interface UtxoItem {
|
||||
outpoint: string;
|
||||
txout: {
|
||||
value: number;
|
||||
script_pubkey: string;
|
||||
};
|
||||
keychain: string;
|
||||
is_spent: boolean;
|
||||
redshifted?: boolean;
|
||||
}
|
||||
|
||||
const dummyRedshift: RedshiftResult = {
|
||||
id: "44036599c37d590899e8d5d920860286",
|
||||
input_utxo:
|
||||
"44036599c37d590899e8d5d92086028695d2c2966fdc354ce1da9a9eac610a53:1",
|
||||
status: "Completed", // Replace with a dummy value for RedshiftStatus
|
||||
recipient: {}, // Replace with a dummy value for RedshiftRecipient
|
||||
output_utxo:
|
||||
"44036599c37d590899e8d5d92086028695d2c2966fdc354ce1da9a9eac610a53:1",
|
||||
introduction_channel:
|
||||
"a7773e57f8595848a635e9af105927cac9ecaf292d71a76456ae0455bd3c9c64:0",
|
||||
output_channel:
|
||||
"a7773e57f8595848a635e9af105927cac9ecaf292d71a76456ae0455bd3c9c64:0",
|
||||
introduction_node: {}, // Replace with a dummy value for PublicKey
|
||||
amount_sats: BigInt(1000000),
|
||||
change_amt: BigInt(12345),
|
||||
fees_paid: BigInt(2500)
|
||||
};
|
||||
|
||||
function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const getUtXos = async () => {
|
||||
console.log("Getting utxos");
|
||||
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[];
|
||||
};
|
||||
|
||||
// function findUtxoByOutpoint(
|
||||
// outpoint?: string,
|
||||
// utxos: UtxoItem[] = []
|
||||
// ): UtxoItem | undefined {
|
||||
// if (!outpoint) return undefined
|
||||
// return utxos.find((utxo) => utxo.outpoint === outpoint)
|
||||
// }
|
||||
|
||||
const [_utxos, { refetch: _refetchUtxos }] = createResource(getUtXos);
|
||||
|
||||
// const inputUtxo = createMemo(() => {
|
||||
// console.log(utxos())
|
||||
// const foundUtxo = findUtxoByOutpoint(props.redshift.input_utxo, utxos())
|
||||
// console.log("Found utxo:", foundUtxo)
|
||||
// return foundUtxo
|
||||
// })
|
||||
|
||||
const [redshiftResource, { refetch: _refetchRedshift }] = createResource(
|
||||
async () => {
|
||||
console.log("Checking redshift", props.redshift.id);
|
||||
const redshift = await state.mutiny_wallet?.get_redshift(
|
||||
props.redshift.id
|
||||
);
|
||||
console.log(redshift);
|
||||
return redshift;
|
||||
}
|
||||
);
|
||||
onMount(() => {
|
||||
// const interval = setInterval(() => {
|
||||
// if (redshiftResource()) refetch()
|
||||
// // if (sentAmount() === 200000) {
|
||||
// // clearInterval(interval)
|
||||
// // props.setShiftStage("success");
|
||||
// // // setSentAmount((0))
|
||||
// // } else {
|
||||
// // setSentAmount((sentAmount() + 50000))
|
||||
// // }
|
||||
// }, 1000)
|
||||
});
|
||||
|
||||
// const outputUtxo = createMemo(() => {
|
||||
// return findUtxoByOutpoint(redshiftResource()?.output_utxo, utxos())
|
||||
// })
|
||||
|
||||
createEffect(() => {
|
||||
setRedshifted(true, redshiftResource()?.output_utxo);
|
||||
});
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
|
||||
return (
|
||||
<VStack biggap>
|
||||
{/* <VStack>
|
||||
<NiceP>We did it. Here's your new UTXO:</NiceP>
|
||||
<Show when={utxos() && outputUtxo()}>
|
||||
<Card>
|
||||
<Utxo item={outputUtxo()!} />
|
||||
</Card>
|
||||
</Show>
|
||||
</VStack> */}
|
||||
<VStack>
|
||||
<NiceP>{i18n.t("redshift.what_happened")}</NiceP>
|
||||
<Show when={redshiftResource()}>
|
||||
<Card>
|
||||
<VStack biggap>
|
||||
{/* <KV key="Input utxo">
|
||||
<Show when={utxos() && inputUtxo()}>
|
||||
<Utxo item={inputUtxo()!} />
|
||||
</Show>
|
||||
</KV> */}
|
||||
<KV key={i18n.t("redshift.starting_amount")}>
|
||||
<AmountSats
|
||||
amountSats={redshiftResource()!.amount_sats}
|
||||
/>
|
||||
</KV>
|
||||
<KV key={i18n.t("redshift.fees_paid")}>
|
||||
<AmountSats
|
||||
amountSats={redshiftResource()!.fees_paid}
|
||||
/>
|
||||
</KV>
|
||||
<KV key={i18n.t("redshift.change")}>
|
||||
<AmountSats
|
||||
amountSats={redshiftResource()!.change_amt}
|
||||
/>
|
||||
</KV>
|
||||
<KV key={i18n.t("redshift.outbound_channel")}>
|
||||
<VStack>
|
||||
<pre class="whitespace-pre-wrap break-all">
|
||||
{
|
||||
redshiftResource()!
|
||||
.introduction_channel
|
||||
}
|
||||
</pre>
|
||||
<a
|
||||
class=""
|
||||
href={mempoolTxUrl(
|
||||
redshiftResource()!.introduction_channel?.split(
|
||||
":"
|
||||
)[0],
|
||||
network
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{i18n.t("common.view_transaction")}
|
||||
</a>
|
||||
</VStack>
|
||||
</KV>
|
||||
<Show when={redshiftResource()!.output_channel}>
|
||||
<KV key={i18n.t("redshift.return_channel")}>
|
||||
<VStack>
|
||||
<pre class="whitespace-pre-wrap break-all">
|
||||
{redshiftResource()!.output_channel}
|
||||
</pre>
|
||||
<a
|
||||
class=""
|
||||
href={mempoolTxUrl(
|
||||
redshiftResource()!.output_channel?.split(
|
||||
":"
|
||||
)[0],
|
||||
network
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{i18n.t("common.view_transaction")}
|
||||
</a>
|
||||
</VStack>
|
||||
</KV>
|
||||
</Show>
|
||||
</VStack>
|
||||
</Card>
|
||||
</Show>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
||||
const i18n = useI18n();
|
||||
const redshifted = createMemo(() => getRedshifted(props.item.outpoint));
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
class={THREE_COLUMNS}
|
||||
onClick={() => props.onClick && props.onClick()}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<img src={utxoIcon} alt="coin" />
|
||||
</div>
|
||||
<div class={CENTER_COLUMN}>
|
||||
<div class="flex gap-2">
|
||||
<Show
|
||||
when={redshifted()}
|
||||
fallback={
|
||||
<h2 class={MISSING_LABEL}>
|
||||
{i18n.t("redshift.unknown")}
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<h2 class={REDSHIFT_LABEL}>
|
||||
{i18n.t("redshift.title")}
|
||||
</h2>
|
||||
</Show>
|
||||
</div>
|
||||
<SmallAmount amount={props.item.txout.value} />
|
||||
</div>
|
||||
<div class={RIGHT_COLUMN}>
|
||||
<SmallHeader
|
||||
class={
|
||||
props.item?.is_spent ? "text-m-red" : "text-m-green"
|
||||
}
|
||||
>
|
||||
{/* {props.item?.is_spent ? "SPENT" : "UNSPENT"} */}
|
||||
</SmallHeader>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const FAKE_STATES = [
|
||||
"Creating a new node",
|
||||
"Opening a channel",
|
||||
"Sending funds through",
|
||||
"Closing the channel",
|
||||
"Redshift complete"
|
||||
];
|
||||
|
||||
function ShiftObserver(props: {
|
||||
setShiftStage: (stage: ShiftStage) => void;
|
||||
redshiftId: string;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [_state, _actions] = useMegaStore();
|
||||
|
||||
const [fakeStage, _setFakeStage] = createSignal(2);
|
||||
|
||||
const [sentAmount, setSentAmount] = createSignal(0);
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (sentAmount() === 200000) {
|
||||
clearInterval(interval);
|
||||
props.setShiftStage("success");
|
||||
// setSentAmount((0))
|
||||
} else {
|
||||
setSentAmount(sentAmount() + 50000);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// async function checkRedshift(id: string) {
|
||||
// console.log("Checking redshift", id)
|
||||
// const redshift = await state.mutiny_wallet?.get_redshift(id)
|
||||
// console.log(redshift)
|
||||
// return redshift
|
||||
// }
|
||||
|
||||
// const [redshiftResource, { refetch }] = createResource(
|
||||
// props.redshiftId,
|
||||
// checkRedshift
|
||||
// )
|
||||
|
||||
// onMount(() => {
|
||||
// const interval = setInterval(() => {
|
||||
// if (redshiftResource()) refetch();
|
||||
// // if (sentAmount() === 200000) {
|
||||
// // clearInterval(interval)
|
||||
// // props.setShiftStage("success");
|
||||
// // // setSentAmount((0))
|
||||
|
||||
// // } else {
|
||||
// // setSentAmount((sentAmount() + 50000))
|
||||
// // }
|
||||
// }, 1000)
|
||||
// })
|
||||
|
||||
// createEffect(() => {
|
||||
// const interval = setInterval(() => {
|
||||
// if (chosenUtxo()) refetch();
|
||||
// }, 1000); // Poll every second
|
||||
// onCleanup(() => {
|
||||
// clearInterval(interval);
|
||||
// });
|
||||
// });
|
||||
|
||||
return (
|
||||
<>
|
||||
<NiceP>{i18n.t("redshift.watch_it_go")}</NiceP>
|
||||
<Card>
|
||||
<VStack>
|
||||
<pre class="self-center">{FAKE_STATES[fakeStage()]}</pre>
|
||||
<ProgressBar value={sentAmount()} max={200000} />
|
||||
<img src={wave} class="h-4 self-center" alt="sine wave" />
|
||||
</VStack>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const KV: ParentComponent<{ key: string }> = (props) => {
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm font-semibold uppercase">{props.key}</p>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Redshift() {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose");
|
||||
const [shiftType, setShiftType] = createSignal<ShiftOption>("utxo");
|
||||
|
||||
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>();
|
||||
|
||||
const SHIFT_OPTIONS = [
|
||||
{
|
||||
value: "utxo",
|
||||
label: i18n.t("redshift.utxo_label"),
|
||||
caption: i18n.t("redshift.utxo_caption")
|
||||
},
|
||||
{
|
||||
value: "lightning",
|
||||
label: i18n.t("redshift.lightning_label"),
|
||||
caption: i18n.t("redshift.lightning_caption")
|
||||
}
|
||||
];
|
||||
|
||||
const getUtXos = async () => {
|
||||
console.log("Getting utxos");
|
||||
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[];
|
||||
};
|
||||
|
||||
// TODO: FIXME: this is old code needs to be revisited!
|
||||
const getChannels = async () => {
|
||||
console.log("Getting channels");
|
||||
// await state.mutiny_wallet?.sync();
|
||||
const channels =
|
||||
(await state.mutiny_wallet?.list_channels()) as Promise<
|
||||
MutinyChannel[]
|
||||
>;
|
||||
console.log(channels);
|
||||
return channels;
|
||||
};
|
||||
|
||||
const [utxos, { refetch: _refetchUtxos }] = createResource(getUtXos);
|
||||
const [_channels, { refetch: _refetchChannels }] =
|
||||
createResource(getChannels);
|
||||
|
||||
const redshiftedUtxos = createMemo(() => {
|
||||
return utxos()?.filter((utxo) => getRedshifted(utxo.outpoint));
|
||||
});
|
||||
|
||||
const unredshiftedUtxos = createMemo(() => {
|
||||
return utxos()?.filter((utxo) => !getRedshifted(utxo.outpoint));
|
||||
});
|
||||
|
||||
function resetState() {
|
||||
setShiftStage("choose");
|
||||
setShiftType("utxo");
|
||||
setChosenUtxo(undefined);
|
||||
}
|
||||
|
||||
async function redshiftUtxo(utxo: UtxoItem) {
|
||||
console.log("Redshifting utxo", utxo.outpoint);
|
||||
const redshift = await state.mutiny_wallet?.init_redshift(
|
||||
utxo.outpoint
|
||||
);
|
||||
console.log("Redshift initialized:");
|
||||
console.log(redshift);
|
||||
return redshift;
|
||||
}
|
||||
|
||||
const [initializedRedshift, { refetch: _refetchRedshift }] = createResource(
|
||||
chosenUtxo,
|
||||
redshiftUtxo
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
if (chosenUtxo() && initializedRedshift()) {
|
||||
// window.location.href = "/"
|
||||
setShiftStage("observe");
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<BackLink />
|
||||
<LargeHeader>
|
||||
{i18n.t("redshift.title")}{" "}
|
||||
{i18n.t("common.coming_soon")}
|
||||
</LargeHeader>
|
||||
<div class="relative filter grayscale pointer-events-none opacity-75">
|
||||
<VStack biggap>
|
||||
{/* <pre>{JSON.stringify(redshiftResource(), null, 2)}</pre> */}
|
||||
<Switch>
|
||||
<Match when={shiftStage() === "choose"}>
|
||||
<VStack>
|
||||
<NiceP>
|
||||
{i18n.t("redshift.where_this_goes")}
|
||||
</NiceP>
|
||||
<StyledRadioGroup
|
||||
accent="red"
|
||||
value={shiftType()}
|
||||
onValueChange={(newValue) =>
|
||||
setShiftType(
|
||||
newValue as ShiftOption
|
||||
)
|
||||
}
|
||||
choices={SHIFT_OPTIONS}
|
||||
/>
|
||||
</VStack>
|
||||
<VStack>
|
||||
<NiceP>
|
||||
{i18n.t("redshift.choose_your")}{" "}
|
||||
<span class="inline-block">
|
||||
<img
|
||||
class="h-4"
|
||||
src={wave}
|
||||
alt="sine wave"
|
||||
/>
|
||||
</span>{" "}
|
||||
{i18n.t("redshift.utxo_to_begin")}
|
||||
</NiceP>
|
||||
<Suspense>
|
||||
<Card
|
||||
title={i18n.t(
|
||||
"redshift.unshifted_utxo"
|
||||
)}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={utxos.loading}>
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
utxos.state ===
|
||||
"ready" &&
|
||||
unredshiftedUtxos()
|
||||
?.length === 0
|
||||
}
|
||||
>
|
||||
<code>
|
||||
{i18n.t(
|
||||
"redshift.no_utxos_empty_state"
|
||||
)}
|
||||
</code>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
utxos.state ===
|
||||
"ready" &&
|
||||
unredshiftedUtxos() &&
|
||||
unredshiftedUtxos()!
|
||||
.length >= 0
|
||||
}
|
||||
>
|
||||
<For
|
||||
each={unredshiftedUtxos()}
|
||||
>
|
||||
{(utxo) => (
|
||||
<Utxo
|
||||
item={utxo}
|
||||
onClick={() =>
|
||||
setChosenUtxo(
|
||||
utxo
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Card>
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<Card
|
||||
titleElement={
|
||||
<SmallHeader>
|
||||
<span class="text-m-red">
|
||||
{i18n.t(
|
||||
"redshift.redshifted"
|
||||
)}{" "}
|
||||
</span>
|
||||
{i18n.t(
|
||||
"redshift.utxos"
|
||||
)}
|
||||
</SmallHeader>
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={utxos.loading}>
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
utxos.state ===
|
||||
"ready" &&
|
||||
redshiftedUtxos()
|
||||
?.length === 0
|
||||
}
|
||||
>
|
||||
<code>
|
||||
{i18n.t(
|
||||
"redshift.no_utxos_empty_state"
|
||||
)}
|
||||
</code>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
utxos.state ===
|
||||
"ready" &&
|
||||
redshiftedUtxos() &&
|
||||
redshiftedUtxos()!
|
||||
.length >= 0
|
||||
}
|
||||
>
|
||||
<For
|
||||
each={redshiftedUtxos()}
|
||||
>
|
||||
{(utxo) => (
|
||||
<Utxo
|
||||
item={utxo}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Card>
|
||||
</Suspense>
|
||||
</VStack>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
shiftStage() === "observe" &&
|
||||
chosenUtxo()
|
||||
}
|
||||
>
|
||||
<ShiftObserver
|
||||
setShiftStage={setShiftStage}
|
||||
redshiftId="dummy-redshift"
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
shiftStage() === "success" &&
|
||||
chosenUtxo()
|
||||
}
|
||||
>
|
||||
<VStack biggap>
|
||||
<RedshiftReport
|
||||
redshift={dummyRedshift}
|
||||
utxo={chosenUtxo()!}
|
||||
/>
|
||||
<Button
|
||||
intent="red"
|
||||
onClick={resetState}
|
||||
>
|
||||
{i18n.t("common.nice")}
|
||||
</Button>
|
||||
</VStack>
|
||||
</Match>
|
||||
<Match when={shiftStage() === "failure"}>
|
||||
<NiceP>{i18n.t("redshift.oh_dear")}</NiceP>
|
||||
<NiceP>
|
||||
{i18n.t("redshift.here_is_error")}
|
||||
</NiceP>
|
||||
<Button intent="red" onClick={resetState}>
|
||||
{i18n.t("common.dangit")}
|
||||
</Button>
|
||||
</Match>
|
||||
</Switch>
|
||||
</VStack>
|
||||
</div>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="redshift" />
|
||||
</SafeArea>
|
||||
</MutinyWalletGuard>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user