Merge pull request #158 from MutinyWallet/receive-details-min

details modal for onchain
This commit is contained in:
Paul Miller
2023-05-23 13:26:02 -05:00
committed by GitHub
3 changed files with 521 additions and 297 deletions

View File

@@ -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 (
<>
<JsonModal open={open()} data={props.item} title="On-Chain Transaction" setOpen={setOpen}>
<a href={mempoolTxUrl(props.item.txid, props.network)} target="_blank" rel="noreferrer">
Mempool Link
</a>
</JsonModal>
<ActivityItem
kind={"onchain"}
labels={props.labels}
// FIXME: is this something we can put into node logic?
amount={isReceive() ? props.item.received - props.item.sent : props.item.sent - props.item.received}
date={props.item.confirmation_time?.Confirmed?.time}
positive={isReceive()}
onClick={() => setOpen(!open())}
/>
</>
)
return (
<>
<DetailsModal open={open()} data={props.item} setOpen={setOpen} />
<ActivityItem
kind={"onchain"}
labels={props.labels}
// FIXME: is this something we can put into node logic?
amount={
isReceive()
? props.item.received - props.item.sent
: props.item.sent - props.item.received
}
date={props.item.confirmation_time?.Confirmed?.time}
positive={isReceive()}
onClick={() => 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 (
<>
<DetailsModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
<ActivityItem kind={"lightning"} labels={props.labels} amount={props.item.amount_sats || 0n} date={props.item.last_updated} positive={!isSend()} onClick={() => setOpen(!open())} />
</>
)
return (
<>
<DetailsModal open={open()} data={props.item} setOpen={setOpen} />
<ActivityItem
kind={"lightning"}
labels={props.labels}
amount={props.item.amount_sats || 0n}
date={props.item.last_updated}
positive={!isSend()}
onClick={() => 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 (
<>
<JsonModal open={open()} data={props.item} title="Unspent Transaction Output" setOpen={setOpen} />
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
<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}>Unknown</h2>}>
<h2 class={REDSHIFT_LABEL}>Redshift</h2>
</Show>
</div>
<SmallAmount amount={props.item.txout.value} />
</div>
<div class={RIGHT_COLUMN}>
<SmallHeader class={spent() ? "text-m-red" : "text-m-green"}>
{/* {spent() ? "SPENT" : "UNSPENT"} */}
</SmallHeader>
</div>
</div>
</>
)
return (
<>
<JsonModal
open={open()}
data={props.item}
title="Unspent Transaction Output"
setOpen={setOpen}
/>
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
<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}>Unknown</h2>}
>
<h2 class={REDSHIFT_LABEL}>Redshift</h2>
</Show>
</div>
<SmallAmount amount={props.item.txout.value} />
</div>
<div class={RIGHT_COLUMN}>
<SmallHeader class={spent() ? "text-m-red" : "text-m-green"}>
{/* {spent() ? "SPENT" : "UNSPENT"} */}
</SmallHeader>
</div>
</div>
</>
)
}
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 (
<Switch>
<Match when={activity.loading}>
<LoadingSpinner wide />
</Match>
<Match when={activity.state === "ready" && activity().length === 0}>
<NiceP>No activity to show</NiceP>
</Match>
<Match when={activity.state === "ready" && activity().length >= 0}>
<For each={activity.latest}>
{(activityItem) =>
<Switch>
<Match when={activityItem.type === "onchain"}>
<OnChainItem item={activityItem.item as OnChainTx} labels={activityItem.labels} network={network} />
</Match>
<Match when={activityItem.type === "lightning"}>
<InvoiceItem item={activityItem.item as MutinyInvoice} labels={activityItem.labels} />
</Match>
</Switch>
}
</For>
</Match>
</Switch>
return activity
}
)
const [activity, { refetch }] = createResource(getAllActivity)
const network = state.mutiny_wallet?.get_network() as Network
}
createEffect(() => {
// After every sync we should refetch the activity
if (!state.is_syncing) {
refetch()
}
})
return (
<Switch>
<Match when={activity.loading}>
<LoadingSpinner wide />
</Match>
<Match when={activity.state === "ready" && activity().length === 0}>
<NiceP>No activity to show</NiceP>
</Match>
<Match when={activity.state === "ready" && activity().length >= 0}>
<For each={activity.latest}>
{(activityItem) => (
<Switch>
<Match when={activityItem.type === "onchain"}>
<OnChainItem
item={activityItem.item as OnChainTx}
labels={activityItem.labels}
network={network}
/>
</Match>
<Match when={activityItem.type === "lightning"}>
<InvoiceItem
item={activityItem.item as MutinyInvoice}
labels={activityItem.labels}
/>
</Match>
</Switch>
)}
</For>
</Match>
</Switch>
)
}

View File

@@ -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 (
<div class="flex flex-col gap-2">
<h1 class="text-4xl font-light">
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}&nbsp;<span class='text-xl'>SATS</span>
</h1>
<Show when={props.showFiat}>
<h2 class="text-xl font-light text-white/70" >
&#8776; {props.loading ? "..." : amountInUsd()}&nbsp;<span class="text-sm">USD</span>
</h2>
</Show>
</div>
)
}
return (
<div class="flex flex-col gap-2">
<h1 class="text-4xl font-light">
{props.loading ? "..." : prettyPrintAmount(props.amountSats)}&nbsp;
<span class="text-xl">SATS</span>
</h1>
<Show when={props.showFiat}>
<h2 class="text-xl font-light text-white/70">
&#8776; {props.loading ? "..." : amountInUsd()}&nbsp;
<span class="text-sm">USD</span>
</h2>
</Show>
</div>
)
}
export function AmountSmall(props: {
amountSats: bigint | number | undefined
}) {
return (
<span class="font-light">
{prettyPrintAmount(props.amountSats)}&nbsp;
<span class="text-sm">SATS</span>
</span>
)
}

View File

@@ -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 (
<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.is_send ? "Lightning send" : "Lightning receive"}</h1>
<ActivityAmount center amount={props.info.amount_sats?.toString() ?? "0"} price={state.price} positive={!props.info.is_send} />
<For each={tags()}>
{(tag) => (
<TinyButton tag={tag} onClick={() => { }}>
{tag.name}
</TinyButton>
)}
</For>
</div>
)
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.is_send ? "Lightning send" : "Lightning receive"}
</h1>
<ActivityAmount
center
amount={props.info.amount_sats?.toString() ?? "0"}
price={state.price}
positive={!props.info.is_send}
/>
<For each={tags()}>
{(tag) => (
<TinyButton tag={tag} onClick={() => {}}>
{tag.name}
</TinyButton>
)}
</For>
</div>
)
}
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 (
<div class="flex flex-col items-center gap-4">
<div class="p-4 bg-neutral-100 rounded-full">
<img src={chain} alt="blockchain" class="w-8 h-8" />
</div>
<h1 class="uppercase font-semibold">
{isSend() ? "On-chain send" : "On-chain receive"}
</h1>
<ActivityAmount
center
amount={amount() ?? "0"}
price={state.price}
positive={!isSend()}
/>
<For each={tags()}>
{(tag) => (
<TinyButton tag={tag} onClick={() => {}}>
{tag.name}
</TinyButton>
)}
</For>
</div>
)
}
const KeyValue: ParentComponent<{ key: string }> = (props) => {
return (
<li class="flex justify-between items-center gap-4">
<span class="uppercase font-semibold whitespace-nowrap">{props.key}</span>
{props.children}
</li>
)
return (
<li class="flex justify-between items-center gap-4">
<span class="uppercase font-semibold whitespace-nowrap">{props.key}</span>
<span class="font-light">{props.children}</span>
</li>
)
}
function MiniStringShower(props: { text: string }) {
const [copy, _copied] = useCopy({ copiedTimeout: 1000 });
const [copy, _copied] = useCopy({ copiedTimeout: 1000 })
return (
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
<pre class="truncate text-neutral-300">{props.text}</pre>
<button class="w-[1rem]" onClick={() => copy(props.text)}>
<img src={copyIcon} alt="copy" />
</button>
</div>
)
return (
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
<pre class="truncate text-neutral-300 font-light">{props.text}</pre>
<button class="w-[1rem]" onClick={() => copy(props.text)}>
<img src={copyIcon} alt="copy" class="w-4 h-4" />
</button>
</div>
)
}
function LightningDetails(props: { info: MutinyInvoice }) {
return (
<VStack>
<ul class="flex flex-col gap-4">
<KeyValue key="Status">
<span class="text-neutral-300">{props.info.paid ? "Paid" : "Unpaid"}</span>
</KeyValue>
<KeyValue key="When">
<span class="text-neutral-300">{prettyPrintTime(Number(props.info.last_updated))}</span>
</KeyValue>
<Show when={props.info.description}>
<KeyValue key="Description">
<span class="text-neutral-300 truncate">{props.info.description}</span>
</KeyValue>
</Show>
<KeyValue key="Fees">
<span class="text-neutral-300">{props.info.fees_paid?.toLocaleString() ?? 0}</span>
</KeyValue>
<KeyValue key="Bolt11">
<MiniStringShower text={props.info.bolt11 ?? ""} />
</KeyValue>
<KeyValue key="Payment Hash">
<MiniStringShower text={props.info.payment_hash ?? ""} />
</KeyValue>
<KeyValue key="Preimage">
<MiniStringShower text={props.info.preimage ?? ""} />
</KeyValue>
</ul>
</VStack>
)
return (
<VStack>
<ul class="flex flex-col gap-4">
<KeyValue key="Status">
<span class="text-neutral-300">
{props.info.paid ? "Paid" : "Unpaid"}
</span>
</KeyValue>
<KeyValue key="When">
<span class="text-neutral-300">
{prettyPrintTime(Number(props.info.last_updated))}
</span>
</KeyValue>
<Show when={props.info.description}>
<KeyValue key="Description">
<span class="text-neutral-300 truncate">
{props.info.description}
</span>
</KeyValue>
</Show>
<KeyValue key="Fees">
<span class="text-neutral-300">
<AmountSmall amountSats={props.info.fees_paid} />
</span>
</KeyValue>
<KeyValue key="Bolt11">
<MiniStringShower text={props.info.bolt11 ?? ""} />
</KeyValue>
<KeyValue key="Payment Hash">
<MiniStringShower text={props.info.payment_hash ?? ""} />
</KeyValue>
<KeyValue key="Preimage">
<MiniStringShower text={props.info.preimage ?? ""} />
</KeyValue>
</ul>
</VStack>
)
}
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 (
<Dialog.Root open={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
<Dialog.Portal>
<Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>
<div class="flex justify-between mb-2">
<div />
<Dialog.CloseButton>
<button tabindex="-1" class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue ">
<img src={close} alt="Close" class="w-8 h-8" />
</button>
</Dialog.CloseButton>
</div>
<Dialog.Title>
<LightningHeader info={props.data as MutinyInvoice} />
</Dialog.Title>
<Hr />
<Dialog.Description class="flex flex-col gap-4">
<LightningDetails info={props.data as MutinyInvoice} />
<div class="flex justify-center">
<CopyButton title="Copy" text={json()} />
</div>
</Dialog.Description>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root >
)
const confirmationTime = () => {
return props.info.confirmation_time?.Confirmed?.time
}
const network = state.mutiny_wallet?.get_network() as Network
return (
<VStack>
<ul class="flex flex-col gap-4">
<KeyValue key="Status">
<span class="text-neutral-300">
{confirmationTime() ? "Confirmed" : "Unconfirmed"}
</span>
</KeyValue>
<Show when={confirmationTime()}>
<KeyValue key="When">
<span class="text-neutral-300">
{confirmationTime()
? prettyPrintTime(Number(confirmationTime()))
: "Pending"}
</span>
</KeyValue>
</Show>
<KeyValue key="Fee">
<span class="text-neutral-300">
<AmountSmall amountSats={props.info.fee} />
</span>
</KeyValue>
<KeyValue key="Txid">
<MiniStringShower text={props.info.txid ?? ""} />
</KeyValue>
</ul>
<a
class="uppercase font-light text-center"
href={mempoolTxUrl(props.info.txid, network)}
target="_blank"
rel="noreferrer"
>
Mempool.space
</a>
</VStack>
)
}
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 (
<Dialog.Root
open={props.open}
onOpenChange={(isOpen) => props.setOpen(isOpen)}
>
<Dialog.Portal>
<Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>
<div class="flex justify-between mb-2">
<div />
<Dialog.CloseButton>
<button
tabindex="-1"
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue "
>
<img src={close} alt="Close" class="w-8 h-8" />
</button>
</Dialog.CloseButton>
</div>
<Dialog.Title>
<Switch>
<Match when={isInvoice()}>
<LightningHeader info={props.data as MutinyInvoice} />
</Match>
<Match when={true}>
<OnchainHeader info={props.data as OnChainTx} />
</Match>
</Switch>
</Dialog.Title>
<Hr />
<Dialog.Description class="flex flex-col gap-4">
<Switch>
<Match when={isInvoice()}>
<LightningDetails info={props.data as MutinyInvoice} />
</Match>
<Match when={true}>
<OnchainDetails info={props.data as OnChainTx} />
</Match>
</Switch>
<div class="flex justify-center">
<CopyButton title="Copy" text={json()} />
</div>
</Dialog.Description>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
)
}