move types from Activity component

This commit is contained in:
gawlk
2023-08-18 13:18:37 +02:00
committed by Paul Miller
parent 8fa30119e1
commit 6a8e61c926
3 changed files with 1343 additions and 0 deletions

161
src/components/Activity.tsx Normal file
View 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>
);
}

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