From 6a8e61c926c63373600a45b2b88af2f635806340 Mon Sep 17 00:00:00 2001 From: gawlk Date: Fri, 18 Aug 2023 13:18:37 +0200 Subject: [PATCH] move types from Activity component --- src/components/Activity.tsx | 161 ++++++++ src/components/DetailsModal.tsx | 530 ++++++++++++++++++++++++++ src/routes/Redshift.tsx | 652 ++++++++++++++++++++++++++++++++ 3 files changed, 1343 insertions(+) create mode 100644 src/components/Activity.tsx create mode 100644 src/components/DetailsModal.tsx create mode 100644 src/routes/Redshift.tsx diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx new file mode 100644 index 00000000..736ac1ac --- /dev/null +++ b/src/components/Activity.tsx @@ -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 ( + + ); +} + +export function CombinedActivity(props: { limit?: number }) { + const [state, _actions] = useMegaStore(); + const i18n = useI18n(); + + const [detailsOpen, setDetailsOpen] = createSignal(false); + const [detailsKind, setDetailsKind] = createSignal(); + 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 ( + } + > + + + + + +
+ + {i18n.t( + "activity.receive_some_sats_to_get_started" + )} + +
+
+ props.limit} + > + + {(activityItem) => ( + + )} + + + = 0}> + + {(activityItem) => ( + + )} + + +
+ 0}> + + {i18n.t("activity.view_all")} + + +
+ ); +} diff --git a/src/components/DetailsModal.tsx b/src/components/DetailsModal.tsx new file mode 100644 index 00000000..b49d7b33 --- /dev/null +++ b/src/components/DetailsModal.tsx @@ -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 ( +
+
+ lightning bolt +
+

+ {props.info.inbound + ? i18n.t("modals.transaction_details.lightning_receive") + : i18n.t("modals.transaction_details.lightning_send")} +

+ + + {(tag) => ( + { + // noop + }} + > + {tag.name} + + )} + +
+ ); +} + +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 ( +
+
+ + + swap + + + blockchain + + +
+

+ {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")} +

+ + + + + {(tag) => ( + { + // noop + }} + > + {tag.name} + + )} + +
+ ); +} + +export const KeyValue: ParentComponent<{ key: string }> = (props) => { + return ( +
  • + + {props.key} + + {props.children} +
  • + ); +}; + +export function MiniStringShower(props: { text: string }) { + const [copy, copied] = useCopy({ copiedTimeout: 1000 }); + + return ( +
    + + {/*
    {props.text}
    */} + +
    + ); +} + +function LightningDetails(props: { info: MutinyInvoice }) { + const i18n = useI18n(); + return ( + +
      + + + {props.info.paid + ? i18n.t("modals.transaction_details.paid") + : i18n.t("modals.transaction_details.unpaid")} + + + + + {prettyPrintTime(Number(props.info.last_updated))} + + + + + + {props.info.description} + + + + + + + + + + + + + + + + + +
    +
    + ); +} + +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 ( + + {/*
    {JSON.stringify(channelInfo() || "", null, 2)}
    */} +
      + + + {confirmationTime() + ? i18n.t("modals.transaction_details.confirmed") + : i18n.t("modals.transaction_details.unconfirmed")} + + + + + + {confirmationTime() + ? prettyPrintTime(Number(confirmationTime())) + : "Pending"} + + + + 0}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {i18n.t("modals.transaction_details.no_details")} + + + +
    +
    + + {i18n.t("common.view_transaction")} + +
    +
    + ); +} + +function ChannelCloseDetails(props: { info: ChannelClosure }) { + const i18n = useI18n(); + return ( + + {/*
    {JSON.stringify(props.info.value, null, 2)}
    */} +
      + + + + + + + {props.info.timestamp + ? prettyPrintTime(Number(props.info.timestamp)) + : i18n.t("common.pending")} + + + + +

      + {props.info.reason ?? ""} +

      +
      +
    +
    + ); +} + +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 ( + + + +
    + + +
    +
    + + + +
    + + + + + + + + + + +
    + + + + + + + + + + + + + +
    + +
    +
    +
    + + +
    + + + ); +} diff --git a/src/routes/Redshift.tsx b/src/routes/Redshift.tsx new file mode 100644 index 00000000..c8a10f10 --- /dev/null +++ b/src/routes/Redshift.tsx @@ -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 ( + + {/* + We did it. Here's your new UTXO: + + + + + + */} + + {i18n.t("redshift.what_happened")} + + + + {/* + + + + */} + + + + + + + + + + + +
    +                                        {
    +                                            redshiftResource()!
    +                                                .introduction_channel
    +                                        }
    +                                    
    + + {i18n.t("common.view_transaction")} + +
    +
    + + + +
    +                                            {redshiftResource()!.output_channel}
    +                                        
    + + {i18n.t("common.view_transaction")} + +
    +
    +
    +
    +
    +
    +
    +
    + ); +} + +export function Utxo(props: { item: UtxoItem; onClick?: () => void }) { + const i18n = useI18n(); + const redshifted = createMemo(() => getRedshifted(props.item.outpoint)); + return ( + <> +
    props.onClick && props.onClick()} + > +
    + coin +
    +
    +
    + + {i18n.t("redshift.unknown")} + + } + > +

    + {i18n.t("redshift.title")} +

    +
    +
    + +
    +
    + + {/* {props.item?.is_spent ? "SPENT" : "UNSPENT"} */} + +
    +
    + + ); +} + +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 ( + <> + {i18n.t("redshift.watch_it_go")} + + +
    {FAKE_STATES[fakeStage()]}
    + + sine wave +
    +
    + + ); +} + +const KV: ParentComponent<{ key: string }> = (props) => { + return ( +
    +

    {props.key}

    + {props.children} +
    + ); +}; + +export default function Redshift() { + const i18n = useI18n(); + const [state, _actions] = useMegaStore(); + + const [shiftStage, setShiftStage] = createSignal("choose"); + const [shiftType, setShiftType] = createSignal("utxo"); + + const [chosenUtxo, setChosenUtxo] = createSignal(); + + 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 ( + + + + + + {i18n.t("redshift.title")}{" "} + {i18n.t("common.coming_soon")} + +
    + + {/*
    {JSON.stringify(redshiftResource(), null, 2)}
    */} + + + + + {i18n.t("redshift.where_this_goes")} + + + setShiftType( + newValue as ShiftOption + ) + } + choices={SHIFT_OPTIONS} + /> + + + + {i18n.t("redshift.choose_your")}{" "} + + sine wave + {" "} + {i18n.t("redshift.utxo_to_begin")} + + + + + + + + + + {i18n.t( + "redshift.no_utxos_empty_state" + )} + + + = 0 + } + > + + {(utxo) => ( + + setChosenUtxo( + utxo + ) + } + /> + )} + + + + + + + + + {i18n.t( + "redshift.redshifted" + )}{" "} + + {i18n.t( + "redshift.utxos" + )} + + } + > + + + + + + + {i18n.t( + "redshift.no_utxos_empty_state" + )} + + + = 0 + } + > + + {(utxo) => ( + + )} + + + + + + + + + + + + + + + + + + {i18n.t("redshift.oh_dear")} + + {i18n.t("redshift.here_is_error")} + + + + +
    +
    +
    + +
    +
    + ); +}