diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index e6d4c61..39a0e48 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -1,194 +1,246 @@ -import { LoadingSpinner, NiceP, SmallAmount, SmallHeader } from './layout'; -import { For, Match, Show, Switch, createEffect, createMemo, createResource, createSignal } from 'solid-js'; -import { useMegaStore } from '~/state/megaStore'; -import { MutinyInvoice } from '@mutinywallet/mutiny-wasm'; -import { JsonModal } from '~/components/JsonModal'; -import mempoolTxUrl from '~/utils/mempoolTxUrl'; -import utxoIcon from '~/assets/icons/coin.svg'; -import { getRedshifted } from '~/utils/fakeLabels'; -import { ActivityItem } from './ActivityItem'; -import { MutinyTagItem } from '~/utils/tags'; -import { Network } from '~/logic/mutinyWalletSetup'; -import { DetailsModal } from './DetailsModal'; +import { LoadingSpinner, NiceP, SmallAmount, SmallHeader } from "./layout" +import { + For, + Match, + Show, + Switch, + createEffect, + createMemo, + createResource, + createSignal, +} from "solid-js" +import { useMegaStore } from "~/state/megaStore" +import { MutinyInvoice } from "@mutinywallet/mutiny-wasm" +import { JsonModal } from "~/components/JsonModal" +import utxoIcon from "~/assets/icons/coin.svg" +import { getRedshifted } from "~/utils/fakeLabels" +import { ActivityItem } from "./ActivityItem" +import { MutinyTagItem } from "~/utils/tags" +import { Network } from "~/logic/mutinyWalletSetup" +import { DetailsModal } from "./DetailsModal" -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]' +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]" export type OnChainTx = { - txid: string - received: number - sent: number - fee?: number - confirmation_time?: { - "Confirmed"?: { - height: number - time: number - } - }, - labels: string[] + txid: string + received: number + sent: number + fee?: number + confirmation_time?: { + Confirmed?: { + height: number + time: number + } + } + labels: string[] } export type UtxoItem = { - outpoint: string - txout: { - value: number - script_pubkey: string - } - keychain: string - is_spent: boolean, - redshifted?: boolean, + outpoint: string + txout: { + value: number + script_pubkey: string + } + keychain: string + is_spent: boolean + redshifted?: boolean } -function OnChainItem(props: { item: OnChainTx, labels: MutinyTagItem[], network: Network }) { - const isReceive = () => props.item.received > props.item.sent +function OnChainItem(props: { + item: OnChainTx + labels: MutinyTagItem[] + network: Network +}) { + const isReceive = () => props.item.received > props.item.sent - const [open, setOpen] = createSignal(false) + const [open, setOpen] = createSignal(false) - return ( - <> - - - Mempool Link - - - setOpen(!open())} - /> - - ) + return ( + <> + + setOpen(!open())} + /> + + ) } -function InvoiceItem(props: { item: MutinyInvoice, labels: MutinyTagItem[] }) { - const isSend = createMemo(() => props.item.is_send); +function InvoiceItem(props: { item: MutinyInvoice; labels: MutinyTagItem[] }) { + const isSend = createMemo(() => props.item.is_send) - const [open, setOpen] = createSignal(false) + const [open, setOpen] = createSignal(false) - return ( - <> - - setOpen(!open())} /> - - ) + return ( + <> + + setOpen(!open())} + /> + + ) } function Utxo(props: { item: UtxoItem }) { - const spent = createMemo(() => props.item.is_spent); + const spent = createMemo(() => props.item.is_spent) - const [open, setOpen] = createSignal(false) + const [open, setOpen] = createSignal(false) - const redshifted = createMemo(() => getRedshifted(props.item.outpoint)); + const redshifted = createMemo(() => getRedshifted(props.item.outpoint)) - return ( - <> - -
setOpen(!open())}> -
- coin -
-
-
- Unknown}> -

Redshift

-
-
- -
-
- - {/* {spent() ? "SPENT" : "UNSPENT"} */} - -
-
- - ) + return ( + <> + +
setOpen(!open())}> +
+ coin +
+
+
+ Unknown} + > +

Redshift

+
+
+ +
+
+ + {/* {spent() ? "SPENT" : "UNSPENT"} */} + +
+
+ + ) } -type ActivityItem = { type: "onchain" | "lightning", item: OnChainTx | MutinyInvoice, time: number, labels: MutinyTagItem[] } +type ActivityItem = { + type: "onchain" | "lightning" + item: OnChainTx | MutinyInvoice + time: number + labels: MutinyTagItem[] +} function sortByTime(a: ActivityItem, b: ActivityItem) { - return b.time - a.time; + return b.time - a.time } export function CombinedActivity(props: { limit?: number }) { - const [state, actions] = useMegaStore(); + const [state, actions] = useMegaStore() - const getAllActivity = async () => { - console.log("Getting all activity"); - const txs = await state.mutiny_wallet?.list_onchain() as OnChainTx[]; - const invoices = await state.mutiny_wallet?.list_invoices() as MutinyInvoice[]; - const tags = await actions.listTags(); + const getAllActivity = async () => { + console.log("Getting all activity") + const txs = (await state.mutiny_wallet?.list_onchain()) as OnChainTx[] + const invoices = + (await state.mutiny_wallet?.list_invoices()) as MutinyInvoice[] + const tags = await actions.listTags() - let activity: ActivityItem[] = []; + let activity: ActivityItem[] = [] - for (let i = 0; i < txs.length; i++) { - activity.push({ type: "onchain", item: txs[i], time: txs[i].confirmation_time?.Confirmed?.time || Date.now(), labels: [] }) - } - - for (let i = 0; i < invoices.length; i++) { - if (invoices[i].paid) { - activity.push({ type: "lightning", item: invoices[i], time: Number(invoices[i].last_updated), labels: [] }) - } - } - - if (props.limit) { - activity = activity.sort(sortByTime).slice(0, props.limit); - } else { - activity.sort(sortByTime); - } - - for (let i = 0; i < activity.length; i++) { - // filter the tags to only include the ones that have an id matching one of the labels - activity[i].labels = tags.filter((tag) => activity[i].item.labels.includes(tag.id)); - } - - return activity; + for (let i = 0; i < txs.length; i++) { + activity.push({ + type: "onchain", + item: txs[i], + time: txs[i].confirmation_time?.Confirmed?.time || Date.now(), + labels: [], + }) } - const [activity, { refetch }] = createResource(getAllActivity); + for (let i = 0; i < invoices.length; i++) { + if (invoices[i].paid) { + activity.push({ + type: "lightning", + item: invoices[i], + time: Number(invoices[i].last_updated), + labels: [], + }) + } + } - const network = state.mutiny_wallet?.get_network() as Network; + if (props.limit) { + activity = activity.sort(sortByTime).slice(0, props.limit) + } else { + activity.sort(sortByTime) + } - createEffect(() => { - // After every sync we should refetch the activity - if (!state.is_syncing) { - refetch(); - } - }) + for (let i = 0; i < activity.length; i++) { + // filter the tags to only include the ones that have an id matching one of the labels + activity[i].labels = tags.filter((tag) => + activity[i].item.labels.includes(tag.id) + ) + } - return ( - - - - - - No activity to show - - = 0}> - - {(activityItem) => - - - - - - - - - } - - - + return activity + } - ) + const [activity, { refetch }] = createResource(getAllActivity) + const network = state.mutiny_wallet?.get_network() as Network -} \ No newline at end of file + createEffect(() => { + // After every sync we should refetch the activity + if (!state.is_syncing) { + refetch() + } + }) + + return ( + + + + + + No activity to show + + = 0}> + + {(activityItem) => ( + + + + + + + + + )} + + + + ) +} diff --git a/src/components/Amount.tsx b/src/components/Amount.tsx index 5d21e16..289d4c6 100644 --- a/src/components/Amount.tsx +++ b/src/components/Amount.tsx @@ -3,27 +3,45 @@ import { useMegaStore } from "~/state/megaStore" import { satsToUsd } from "~/utils/conversions" function prettyPrintAmount(n?: number | bigint): string { - if (!n || n.valueOf() === 0) { - return "0" - } - return n.toLocaleString() + if (!n || n.valueOf() === 0) { + return "0" + } + return n.toLocaleString() } -export function Amount(props: { amountSats: bigint | number | undefined, showFiat?: boolean, loading?: boolean }) { - const [state, _] = useMegaStore() +export function Amount(props: { + amountSats: bigint | number | undefined + showFiat?: boolean + loading?: boolean +}) { + const [state, _] = useMegaStore() - const amountInUsd = () => satsToUsd(state.price, Number(props.amountSats) || 0, true) + const amountInUsd = () => + satsToUsd(state.price, Number(props.amountSats) || 0, true) - return ( -
-

- {props.loading ? "..." : prettyPrintAmount(props.amountSats)} SATS -

- -

- ≈ {props.loading ? "..." : amountInUsd()} USD -

-
-
- ) -} \ No newline at end of file + return ( +
+

+ {props.loading ? "..." : prettyPrintAmount(props.amountSats)}  + SATS +

+ +

+ ≈ {props.loading ? "..." : amountInUsd()}  + USD +

+
+
+ ) +} + +export function AmountSmall(props: { + amountSats: bigint | number | undefined +}) { + return ( + + {prettyPrintAmount(props.amountSats)}  + SATS + + ) +} diff --git a/src/components/DetailsModal.tsx b/src/components/DetailsModal.tsx index 4f08c09..0737893 100644 --- a/src/components/DetailsModal.tsx +++ b/src/components/DetailsModal.tsx @@ -1,145 +1,299 @@ -import { Dialog } from "@kobalte/core"; -import { For, JSX, ParentComponent, Show, createMemo } from "solid-js"; -import { Hr, TinyButton, VStack } from "~/components/layout"; -import { MutinyInvoice } from "@mutinywallet/mutiny-wasm"; -import { OnChainTx } from "./Activity"; +import { Dialog } from "@kobalte/core" +import { + For, + JSX, + Match, + ParentComponent, + Show, + Switch, + createMemo, +} from "solid-js" +import { Hr, TinyButton, VStack } from "~/components/layout" +import { MutinyInvoice } from "@mutinywallet/mutiny-wasm" +import { OnChainTx } from "./Activity" -import close from "~/assets/icons/close.svg"; -import bolt from "~/assets/icons/bolt-black.svg"; +import close from "~/assets/icons/close.svg" +import bolt from "~/assets/icons/bolt-black.svg" +import chain from "~/assets/icons/chain-black.svg" import copyIcon from "~/assets/icons/copy.svg" -import { ActivityAmount } from "./ActivityItem"; -import { CopyButton } from "./ShareCard"; -import { prettyPrintTime } from "~/utils/prettyPrintTime"; -import { useMegaStore } from "~/state/megaStore"; -import { tagToMutinyTag } from "~/utils/tags"; -import { useCopy } from "~/utils/useCopy"; +import { ActivityAmount } from "./ActivityItem" +import { CopyButton } from "./ShareCard" +import { prettyPrintTime } from "~/utils/prettyPrintTime" +import { useMegaStore } from "~/state/megaStore" +import { tagToMutinyTag } from "~/utils/tags" +import { useCopy } from "~/utils/useCopy" +import mempoolTxUrl from "~/utils/mempoolTxUrl" +import { Network } from "~/logic/mutinyWalletSetup" +import { AmountSmall } from "./Amount" const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm" const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center" -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" +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 }) { - const [state, _actions] = useMegaStore(); + const [state, _actions] = useMegaStore() - const tags = createMemo(() => { - if (props.info.labels.length) { - let contact = state.mutiny_wallet?.get_contact(props.info.labels[0]); - if (contact) { - return [tagToMutinyTag(contact)] - } else { - return [] - } - } else { - return [] - } - }) + const tags = createMemo(() => { + if (props.info.labels.length) { + let contact = state.mutiny_wallet?.get_contact(props.info.labels[0]) + if (contact) { + return [tagToMutinyTag(contact)] + } else { + return [] + } + } else { + return [] + } + }) - return ( -
-
- lightning bolt -
-

{props.info.is_send ? "Lightning send" : "Lightning receive"}

- - - {(tag) => ( - { }}> - {tag.name} - - )} - -
- ) + return ( +
+
+ lightning bolt +
+

+ {props.info.is_send ? "Lightning send" : "Lightning receive"} +

+ + + {(tag) => ( + {}}> + {tag.name} + + )} + +
+ ) +} + +function OnchainHeader(props: { info: OnChainTx }) { + const [state, _actions] = useMegaStore() + + const tags = createMemo(() => { + if (props.info.labels.length) { + let contact = state.mutiny_wallet?.get_contact(props.info.labels[0]) + if (contact) { + return [tagToMutinyTag(contact)] + } else { + return [] + } + } else { + return [] + } + }) + + 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 ( +
+
+ blockchain +
+

+ {isSend() ? "On-chain send" : "On-chain receive"} +

+ + + {(tag) => ( + {}}> + {tag.name} + + )} + +
+ ) } const KeyValue: ParentComponent<{ key: string }> = (props) => { - return ( -
  • - {props.key} - {props.children} -
  • - - ) - + return ( +
  • + {props.key} + {props.children} +
  • + ) } function MiniStringShower(props: { text: string }) { - const [copy, _copied] = useCopy({ copiedTimeout: 1000 }); + const [copy, _copied] = useCopy({ copiedTimeout: 1000 }) - return ( -
    -
    {props.text}
    - -
    - ) + return ( +
    +
    {props.text}
    + +
    + ) } function LightningDetails(props: { info: MutinyInvoice }) { - return ( - -
      - - {props.info.paid ? "Paid" : "Unpaid"} - - - {prettyPrintTime(Number(props.info.last_updated))} - - - - {props.info.description} - - - - {props.info.fees_paid?.toLocaleString() ?? 0} - - - - - - - - - - -
    -
    - - ) + return ( + +
      + + + {props.info.paid ? "Paid" : "Unpaid"} + + + + + {prettyPrintTime(Number(props.info.last_updated))} + + + + + + {props.info.description} + + + + + + + + + + + + + + + + + +
    +
    + ) } -export function DetailsModal(props: { title: string, open: boolean, data?: MutinyInvoice | OnChainTx, setOpen: (open: boolean) => void, children?: JSX.Element }) { - const json = createMemo(() => JSON.stringify(props.data, null, 2)); +function OnchainDetails(props: { info: OnChainTx }) { + const [state, _actions] = useMegaStore() - return ( - props.setOpen(isOpen)}> - - -
    - -
    -
    - - - -
    - - - -
    - - -
    - -
    -
    - -
    - - - ) + const confirmationTime = () => { + return props.info.confirmation_time?.Confirmed?.time + } + + const network = state.mutiny_wallet?.get_network() as Network + + return ( + +
      + + + {confirmationTime() ? "Confirmed" : "Unconfirmed"} + + + + + + {confirmationTime() + ? prettyPrintTime(Number(confirmationTime())) + : "Pending"} + + + + + + + + + + + +
    + + Mempool.space + +
    + ) +} + +export function DetailsModal(props: { + open: boolean + data: MutinyInvoice | OnChainTx + setOpen: (open: boolean) => void + children?: JSX.Element +}) { + const json = createMemo(() => JSON.stringify(props.data, null, 2)) + + const isInvoice = () => { + return ("bolt11" in props.data) as boolean + } + + return ( + props.setOpen(isOpen)} + > + + +
    + +
    +
    + + + +
    + + + + + + + + + + +
    + + + + + + + + + +
    + +
    +
    + +
    + + + ) }